/// <summary> /// Reads and deserializes the Vault object located at the specified path as JSON, /// returning the default value if the path doesn't exist. /// </summary> /// <typeparam name="T">The type being read.</typeparam> /// <param name="path">The object path.</param> /// <param name="cancellationToken">The optional <see cref="CancellationToken"/>.</param> /// <returns>The result as a <c>dynamic</c> object or <c>null</c> if the path doesn't exist.</returns> /// <exception cref="HttpException">Thrown for Vault communication problems.</exception> public async Task <T> ReadJsonOrDefaultAsync <T>(string path, CancellationToken cancellationToken = default) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(path)); try { var jsonText = (await jsonClient.GetAsync($"/{vaultApiVersion}/{Normalize(path)}", null, cancellationToken)) .AsDynamic() .data .ToString(); return(NeonHelper.JsonDeserialize <T>(jsonText)); } catch (HttpException e) { if (e.StatusCode == HttpStatusCode.NotFound) { return(default(T)); } throw new HttpException($"[status={e.StatusCode}]: Unable to read Vault bytes from [path={path}]", e); } }
/// <summary> /// Validates the options and also ensures that all <c>null</c> properties are /// initialized to their default values. /// </summary> /// <param name="clusterDefinition">The cluster definition.</param> /// <exception cref="ClusterDefinitionException">Thrown if the definition is not valid.</exception> public void Validate(ClusterDefinition clusterDefinition) { Covenant.Requires <ArgumentNullException>(clusterDefinition != null, nameof(clusterDefinition)); var googleHostingOptionsPrefix = $"{nameof(ClusterDefinition.Hosting)}.{nameof(ClusterDefinition.Hosting.Google)}"; // Verify subnets if (!NetworkCidr.TryParse(VnetSubnet, out var vnetSubnet)) { throw new ClusterDefinitionException($"[{googleHostingOptionsPrefix}.{nameof(VnetSubnet)}={VnetSubnet}] is not a valid subnet."); } if (!NetworkCidr.TryParse(NodeSubnet, out var nodeSubnet)) { throw new ClusterDefinitionException($"[{googleHostingOptionsPrefix}.{nameof(NodeSubnet)}={NodeSubnet}] is not a valid subnet."); } if (!vnetSubnet.Contains(nodeSubnet)) { throw new ClusterDefinitionException($"[{googleHostingOptionsPrefix}.{nameof(NodeSubnet)}={NodeSubnet}] is contained within [{nameof(VnetSubnet)}={VnetSubnet}]."); } }
/// <summary> /// Constructs an instance of <paramref name="resultType"/> from a <see cref="JObject"/>. /// </summary> /// <param name="resultType">The result type.</param> /// <param name="jObject">The source <see cref="JObject"/>.</param> /// <returns>The new instance as an <see cref="object"/>.</returns> public static object CreateFrom(Type resultType, JObject jObject) { Covenant.Requires(resultType != null); Covenant.Requires(jObject != null); #if DEBUG Covenant.Requires <ArgumentException>(resultType.Implements <IRoundtripData>()); #endif MethodInfo createMethod; lock (classNameToJObjectCreateMethod) { if (!classNameToJObjectCreateMethod.TryGetValue(resultType.FullName, out createMethod)) { createMethod = resultType.GetMethod("CreateFrom", BindingFlags.Public | BindingFlags.Static, null, createFromJObjectArgTypes, null); #if DEBUG Covenant.Assert(createMethod != null, $"Cannot locate generated [{resultType.FullName}.CreateFrom(JObject)] method."); #endif classNameToJObjectCreateMethod.Add(resultType.FullName, createMethod); } } return(createMethod.Invoke(null, new object[] { jObject })); }
/// <summary> /// Constructs an instance of <paramref name="resultType"/> from a byte array. /// </summary> /// <param name="resultType">The result type.</param> /// <param name="bytes">The source bytes.</param> /// <returns>The new instance as an <see cref="object"/>.</returns> public static object CreateFrom(Type resultType, byte[] bytes) { Covenant.Requires(resultType != null); Covenant.Requires(bytes != null); var json = Encoding.UTF8.GetString(bytes); // $debug(jeff.lill): DELETE THIS! var jToken = JToken.Parse(json); switch (jToken.Type) { case JTokenType.Null: return(null); case JTokenType.Object: return(CreateFrom(resultType, JObject.Parse(json))); default: throw new ArgumentException("Invalid JSON: Expecting an object or NULL."); } }
public static async Task AssertThrowsAsync <TException>(Func <Task> action) where TException : Exception { await SyncContext.Clear; Covenant.Requires <ArgumentNullException>(action != null, nameof(action)); try { await action(); throw new AssertException($"Expected: {typeof(TException).FullName}\r\nActual: (no exception thrown)"); } catch (Exception e) { if (e is TException || e.Contains <TException>()) { return; } throw new AssertException($"Expected: {typeof(TException).FullName}\r\nActual: {e.GetType().Name}"); } }
/// <summary> /// Asynchronously reads all bytes from the current position to the end of the stream. /// </summary> /// <returns>The byte array.</returns> public static async Task <byte[]> ReadToEndAsync(this Stream stream) { await SyncContext.ClearAsync; Covenant.Requires <ArgumentNullException>(stream != null, nameof(stream)); var buffer = new byte[16 * 1024]; using (var ms = new MemoryStream(16 * 1024)) { while (true) { var cb = await stream.ReadAsync(buffer, 0, buffer.Length); if (cb == 0) { return(ms.ToArray()); } ms.Write(buffer, 0, cb); } } }
/// <summary> /// Connects to a XenServer/XCP-ng host and removes any VMs matching the name or file /// wildcard pattern, forceably shutting the VMs down when necessary. Note that the /// VM's drives will also be removed. /// </summary> /// <param name="addressOrFQDN">Specifies the IP address or hostname for the target XenServer host machine.</param> /// <param name="username">Specifies the username to be used to connect to the host.</param> /// <param name="password">Specifies the host password.</param> /// <param name="nameOrPattern">Specifies the VM name or pattern including '*' or '?' wildcards to be used to remove VMs.</param> public static void RemoveVMs(string addressOrFQDN, string username, string password, string nameOrPattern) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(addressOrFQDN), nameof(addressOrFQDN)); Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(username), nameof(username)); Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(password), nameof(password)); Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(nameOrPattern), nameof(nameOrPattern)); var nameRegex = NeonHelper.FileWildcardRegex(nameOrPattern); using (var client = new XenClient(addressOrFQDN, username, password)) { foreach (var vm in client.Machine.List() .Where(vm => nameRegex.IsMatch(vm.NameLabel))) { if (vm.IsRunning) { client.Machine.Shutdown(vm, turnOff: true); } client.Machine.Remove(vm, keepDrives: false); } } }
/// <summary> /// Constructor. Note that you should dispose the instance when you're finished with it. /// </summary> /// <param name="addressOrFQDN">The target XenServer IP address or FQDN.</param> /// <param name="username">The user name.</param> /// <param name="password">The password.</param> /// <param name="name">Optionally specifies the XenServer name.</param> /// <param name="logFolder"> /// The folder where log files are to be written, otherwise or <c>null</c> or /// empty if logging is disabled. /// </param> public XenClient(string addressOrFQDN, string username, string password, string name = null, string logFolder = null) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(username)); if (!IPAddress.TryParse(addressOrFQDN, out var address)) { var hostEntry = Dns.GetHostEntry(addressOrFQDN); if (hostEntry.AddressList.Length == 0) { throw new XenException($"[{addressOrFQDN}] is not a valid IP address or fully qualified domain name of a XenServer host."); } address = hostEntry.AddressList.First(); } var logWriter = (TextWriter)null; if (!string.IsNullOrEmpty(logFolder)) { Directory.CreateDirectory(logFolder); logWriter = new StreamWriter(Path.Combine(logFolder, $"XENSERVER-{addressOrFQDN}.log")); } Address = addressOrFQDN; Name = name; SshProxy = new SshProxy <XenClient>(addressOrFQDN, null, address, SshCredentials.FromUserPassword(username, password), logWriter); SshProxy.Metadata = this; runOptions = RunOptions.IgnoreRemotePath; // Initialize the operation classes. Repository = new RepositoryOperations(this); Template = new TemplateOperations(this); Machine = new MachineOperations(this); }
/// <summary> /// Starts the controller. /// </summary> /// <param name="k8s">The <see cref="IKubernetes"/> client to use.</param> /// <returns>The tracking <see cref="Task"/>.</returns> public static async Task StartAsync(IKubernetes k8s) { Covenant.Requires <ArgumentNullException>(k8s != null, nameof(k8s)); // Load the configuration settings. var leaderConfig = new LeaderElectionConfig( k8s, @namespace: KubeNamespace.NeonSystem, leaseName: $"{Program.Service.Name}.nodetask", identity: Pod.Name, promotionCounter: Metrics.CreateCounter($"{Program.Service.MetricsPrefix}nodetask_promoted", "Leader promotions"), demotionCounter: Metrics.CreateCounter($"{Program.Service.MetricsPrefix}nodetask_demoted", "Leader demotions"), newLeaderCounter: Metrics.CreateCounter($"{Program.Service.MetricsPrefix}nodetask_newLeader", "Leadership changes")); var options = new ResourceManagerOptions() { IdleInterval = Program.Service.Environment.Get("NODETASK_IDLE_INTERVAL", TimeSpan.FromSeconds(1)), ErrorMinRequeueInterval = Program.Service.Environment.Get("NODETASK_ERROR_MIN_REQUEUE_INTERVAL", TimeSpan.FromSeconds(15)), ErrorMaxRetryInterval = Program.Service.Environment.Get("NODETASK_ERROR_MAX_REQUEUE_INTERVAL", TimeSpan.FromSeconds(60)), IdleCounter = Metrics.CreateCounter($"{Program.Service.MetricsPrefix}nodetask_idle", "IDLE events processed."), ReconcileCounter = Metrics.CreateCounter($"{Program.Service.MetricsPrefix}nodetask_idle", "RECONCILE events processed."), DeleteCounter = Metrics.CreateCounter($"{Program.Service.MetricsPrefix}nodetask_idle", "DELETED events processed."), StatusModifyCounter = Metrics.CreateCounter($"{Program.Service.MetricsPrefix}nodetask_idle", "STATUS-MODIFY events processed."), IdleErrorCounter = Metrics.CreateCounter($"{Program.Service.MetricsPrefix}nodetask_idle_error", "Failed NodeTask IDLE event processing."), ReconcileErrorCounter = Metrics.CreateCounter($"{Program.Service.MetricsPrefix}nodetask_reconcile_error", "Failed NodeTask RECONCILE event processing."), DeleteErrorCounter = Metrics.CreateCounter($"{Program.Service.MetricsPrefix}nodetask_delete_error", "Failed NodeTask DELETE event processing."), StatusModifyErrorCounter = Metrics.CreateCounter($"{Program.Service.MetricsPrefix}nodetask_statusmodify_error", "Failed NodeTask STATUS-MODIFY events processing.") }; resourceManager = new ResourceManager <V1NeonNodeTask, NodeTaskController>( k8s, options: options, leaderConfig: leaderConfig); await resourceManager.StartAsync(); }
/// <summary> /// Returns the Cadence activity type name to be used for a activity interface or /// implementation class. /// </summary> /// <param name="activityType">The activity interface or implementation type.</param> /// <param name="activityAttribute">Specifies the <see cref="ActivityAttribute"/>.</param> /// <returns>The type name.</returns> /// <remarks> /// <para> /// If <paramref name="activityAttribute"/> is passed and <see cref="ActivityAttribute.Name"/> /// is not <c>null</c> or empty, then the name specified in the attribute is returned. /// </para> /// <para> /// Otherwise, we'll return the fully qualified name of the activity interface /// with the leadting "I" removed. /// </para> /// </remarks> internal static string GetActivityTypeName(Type activityType, ActivityAttribute activityAttribute) { Covenant.Requires <ArgumentNullException>(activityType != null, nameof(activityType)); if (activityAttribute != null && !string.IsNullOrEmpty(activityAttribute.Name)) { return(activityAttribute.Name); } if (activityType.IsClass) { CadenceHelper.ValidateActivityImplementation(activityType); activityType = CadenceHelper.GetActivityInterface(activityType); } else { CadenceHelper.ValidateActivityInterface(activityType); } var fullName = activityType.FullName; var name = activityType.Name; if (name.StartsWith("I") && name != "I") { // We're going to strip the leading "I" from the unqualified // type name (unless that's the only character). fullName = fullName.Substring(0, fullName.Length - name.Length); fullName += name.Substring(1); } // We need to replace the "+" characters .NET uses for nested types into // "." so the result will be a valid C# type identifier. return(fullName.Replace('+', '.')); }
/// <summary> /// Restarts the service unless it never been started. /// </summary> /// <param name="serviceCreator">Callback that creates and returns the new service instance.</param> /// <param name="runningTimeout"> /// Optionally specifies the maximum time the fixture should wait for the service to transition /// to the <see cref="NeonServiceStatus.Running"/> state. This defaults to <b>30 seconds</b>. /// </param> /// <exception cref="TimeoutException"> /// Thrown if the service didn't transition to the running (or terminated) state /// within <paramref name="runningTimeout"/>. /// </exception> /// <remarks> /// <para> /// This method first calls the <paramref name="serviceCreator"/> callback and expects /// it to return a new service instance that has been initialized by setting its environment /// variables and configuration files as required. The callback should not start thge service. /// </para> /// </remarks> public void Restart(Func <TService> serviceCreator = null, TimeSpan runningTimeout = default) { Covenant.Requires <ArgumentNullException>(serviceCreator != null, nameof(serviceCreator)); if (Service != null && Service.Status == NeonServiceStatus.NotStarted) { return; } if (runningTimeout == default) { runningTimeout = defaultRunningTimeout; } TerminateService(); ClearCaches(); Service = serviceCreator(); Covenant.Assert(Service != null); serviceTask = Service.RunAsync(); // Wait for the service to signal that it's running or has terminated. try { NeonHelper.WaitFor(() => Service.Status == NeonServiceStatus.Running || Service.Status == NeonServiceStatus.Terminated, runningTimeout); } catch (TimeoutException) { // Throw a nicer exception that explains what's happened in more detail. throw new TimeoutException($"Service [{Service.Name}]'s [{typeof(TService).Name}.OnRunAsync()] method did not call [{nameof(NeonService.StartedAsync)}()] within [{runningTimeout}] indicating that the service is ready. Ensure that [{nameof(NeonService.StartedAsync)}()] is being called or increase the timeout."); } IsRunning = Service.Status == NeonServiceStatus.Running; }
/// <summary> /// Constructor. /// </summary> /// <param name="controller">The setup controller.</param> internal SetupClusterStatus(ISetupController controller) { Covenant.Requires <ArgumentNullException>(controller != null, nameof(controller)); this.isClone = false; this.controller = controller; this.cluster = controller.Get <ClusterProxy>(KubeSetupProperty.ClusterProxy); this.GlobalStatus = controller.GlobalStatus; this.globalStatus = this.GlobalStatus; // Initialize the cluster node/host status instances. this.Nodes = new List <SetupNodeStatus>(); foreach (var node in cluster.Nodes) { Nodes.Add(new SetupNodeStatus(node, node.NodeDefinition)); } this.Hosts = new List <SetupNodeStatus>(); foreach (var host in cluster.Hosts) { Hosts.Add(new SetupNodeStatus(host, new object())); } // Initialize the setup steps. this.Steps = new List <SetupStepStatus>(); foreach (var step in controller.GetStepStatus().Where(step => !step.IsQuiet)) { Steps.Add(step); } this.CurrentStep = Steps.SingleOrDefault(step => step.Number == controller.CurrentStepNumber); }
/// <summary> /// Create the node folders required by neoneKUBE. /// </summary> /// <param name="controller">The setup controller.</param> public void BaseCreateKubeFolders(ISetupController controller) { Covenant.Requires <ArgumentException>(controller != null, nameof(controller)); InvokeIdempotent("base/folders", () => { controller.LogProgress(this, verb: "create", message: "node folders"); var folderScript = $@" set -euo pipefail mkdir -p {KubeNodeFolder.Bin} chmod 750 {KubeNodeFolder.Bin} mkdir -p {KubeNodeFolder.Config} chmod 750 {KubeNodeFolder.Config} mkdir -p {KubeNodeFolder.Setup} chmod 750 {KubeNodeFolder.Setup} mkdir -p {KubeNodeFolder.Helm} chmod 750 {KubeNodeFolder.Helm} mkdir -p {KubeNodeFolder.State} chmod 750 {KubeNodeFolder.State} mkdir -p {KubeNodeFolder.State}/setup chmod 750 {KubeNodeFolder.State}/setup mkdir -p {KubeNodeFolder.NeonRun} chmod 740 {KubeNodeFolder.NeonRun} "; SudoCommand(CommandBundle.FromScript(folderScript), RunOptions.Defaults | RunOptions.FaultOnError); }); }
/// <summary> /// Performs an HTTP <b>POST</b> ensuring that a success code was returned. /// </summary> /// <param name="uri">The URI</param> /// <param name="document">The optional object to be uploaded as the request payload.</param> /// <param name="args">The optional query arguments.</param> /// <param name="headers">The Optional HTTP headers.</param> /// <param name="cancellationToken">The optional <see cref="CancellationToken"/>.</param> /// <param name="logActivity">The optional <see cref="LogActivity"/> whose ID is to be included in the request.</param> /// <returns>The <see cref="JsonResponse"/>.</returns> /// <exception cref="SocketException">Thrown for network connectivity issues.</exception> /// <exception cref="HttpException">Thrown when the server responds with an HTTP error status code.</exception> public async Task <JsonResponse> PostAsync( string uri, object document = null, ArgDictionary args = null, ArgDictionary headers = null, CancellationToken cancellationToken = default, LogActivity logActivity = default) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(uri)); return(await safeRetryPolicy.InvokeAsync( async() => { var requestUri = FormatUri(uri, args); try { var client = this.HttpClient; if (client == null) { throw new ObjectDisposedException(nameof(JsonClient)); } var httpResponse = await client.PostAsync(requestUri, CreateContent(document), cancellationToken: cancellationToken, headers: headers, activity: logActivity); var jsonResponse = new JsonResponse(requestUri, httpResponse, await httpResponse.Content.ReadAsStringAsync()); jsonResponse.EnsureSuccess(); return jsonResponse; } catch (HttpRequestException e) { throw new HttpException(e, requestUri); } })); }
/// <summary> /// Disables an optional Windows feature. /// </summary> /// <exception cref="InvalidOperationException"> /// Thrown if the feature does't exist or is enabled and waiting for a Windows restart. /// </exception> /// <remarks> /// This method does nothing when the feature is already disabled. /// </remarks> public static void DisableOptionalWindowsFeature(string feature) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(feature), nameof(feature)); switch (GetWindowsOptionalFeatureStatus(feature)) { case WindowsFeatureStatus.Unknown: throw new InvalidOperationException($"Unknown Windows Feature: {feature}"); case WindowsFeatureStatus.Enabled: var response = NeonHelper.ExecuteCapture("dism.exe", new object[] { "/Online", "/English", "/Disable-Feature", $"/FeatureName:{feature}" }); response.EnsureSuccess(); break; case WindowsFeatureStatus.EnabledPending: throw new InvalidOperationException($"Windows Feature install is pending: {feature}"); case WindowsFeatureStatus.Disabled: return; default: throw new NotImplementedException(); } }
/// <summary> /// <para> /// Plays a playbook within a specific working directory using <b>neon ansible play -- [args] playbook</b>. /// </para> /// <note> /// This method will have Ansible gather facts by default which can be quite slow. /// Consider using <see cref="PlayInFolderNoGather(string, string, string[])"/> instead /// for unit tests that don't required the facts. /// </note> /// </summary> /// <param name="workDir">The playbook working directory (or <c>null</c> to use a temporary folder).</param> /// <param name="playbook">The playbook text.</param> /// <param name="args">Optional command line arguments to be included in the command.</param> /// <returns>An <see cref="AnsiblePlayResults"/> describing what happened.</returns> /// <remarks> /// <note> /// Use this method for playbooks that need to read or write files. /// </note> /// </remarks> public static AnsiblePlayResults PlayInFolder(string workDir, string playbook, params string[] args) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(playbook)); if (!string.IsNullOrEmpty(workDir)) { Directory.CreateDirectory(workDir); Environment.CurrentDirectory = workDir; File.WriteAllText(Path.Combine(workDir, "play.yaml"), playbook); var response = NeonHelper.ExecuteCapture("neon", new object[] { "ansible", "play", "--noterminal", "--", args, "-vvvv", "play.yaml" }); return(new AnsiblePlayResults(response)); } else { using (var folder = new TempFolder()) { var orgDirectory = Environment.CurrentDirectory; try { Environment.CurrentDirectory = folder.Path; File.WriteAllText(Path.Combine(folder.Path, "play.yaml"), playbook); var response = NeonHelper.ExecuteCapture("neon", new object[] { "ansible", "play", "--noterminal", "--", args, "-vvvv", "play.yaml" }); return(new AnsiblePlayResults(response)); } finally { Environment.CurrentDirectory = orgDirectory; } } } }
/// <summary> /// Disables DHCP. /// </summary> /// <param name="controller">The setup controller.</param> public void BaseDisableDhcp(ISetupController controller) { Covenant.Requires <ArgumentException>(controller != null, nameof(controller)); var hostingEnvironment = controller.Get <HostingEnvironment>(KubeSetupProperty.HostingEnvironment); InvokeIdempotent("base/dhcp", () => { controller.LogProgress(this, verb: "disable", message: "dhcp"); var initNetPlanScript = $@" set -euo pipefail rm -rf /etc/netplan/* cat <<EOF > /etc/netplan/no-dhcp.yaml # This file is used to disable the network when a new VM is created # from a template is booted. The [neon-init] service handles network # provisioning in conjunction with the cluster prepare step. # # Cluster prepare inserts a virtual DVD disc with a script that # handles the network configuration which [neon-init] will # execute. network: version: 2 renderer: networkd ethernets: eth0: dhcp4: no EOF "; SudoCommand(CommandBundle.FromScript(initNetPlanScript), RunOptions.Defaults | RunOptions.FaultOnError); }); }
/// <summary> /// Returns a named broadcast message channel, creating one if it doesn't already /// exist. Broadcast message channels are used to forward messages to one or more /// consumers such that each message is delivered to <b>all consumers</b>. /// </summary> /// <param name="name">The channel name. This can be a maximum of 250 characters.</param> /// <param name="durable"> /// Optionally specifies that the channel should survive message cluster restarts. /// This defaults to <c>false</c>. /// </param> /// <param name="autoDelete"> /// Optionally specifies that channel should be automatically deleted when the /// last consumer is removed. /// </param> /// <param name="messageTTL"> /// <para> /// Optionally specifies the maximum time a message can remain in the channel before /// being deleted. This defaults to <c>null</c> which disables this feature. /// </para> /// <note> /// The maximum possible TTL is about <b>24.855 days</b>. /// </note> /// </param> /// <param name="maxLength"> /// Optionally specifies the maximum number of messages that can be waiting in the channel /// before messages at the front of the channel will be deleted. This defaults /// to unconstrained. /// </param> /// <param name="maxLengthBytes"> /// Optionally specifies the maximum total bytes of messages that can be waiting in /// the channel before messages at the front of the channel will be deleted. This /// defaults to unconstrained. /// </param> /// <param name="publishOnly"> /// Optionally specifies that the channel instance returned will only be able /// to publish messages and not consume them. Enabling this avoid the creation /// of a queue that will unnecessary for this situation. /// </param> /// <returns>The requested <see cref="BroadcastChannel"/>.</returns> /// <remarks> /// <note> /// The instance returned should be disposed when you're done with it. /// </note> /// <note> /// The maximum possible <paramref name="messageTTL"/> is <see cref="int.MaxValue"/> or just /// under 24 days. An <see cref="ArgumentException"/> will be thrown if this is exceeded. /// </note> /// </remarks> public BroadcastChannel GetBroadcastChannel( string name, bool durable = false, bool autoDelete = false, TimeSpan?messageTTL = null, int?maxLength = null, int?maxLengthBytes = null, bool publishOnly = false) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(name)); Covenant.Requires <ArgumentException>(name.Length <= 250); Covenant.Requires <ArgumentException>(!messageTTL.HasValue || messageTTL.Value >= TimeSpan.Zero); Covenant.Requires <ArgumentException>(!maxLength.HasValue || maxLength.Value > 0); Covenant.Requires <ArgumentException>(!maxLengthBytes.HasValue || maxLengthBytes.Value > 0); lock (syncLock) { CheckDisposed(); var channel = new BroadcastChannel( this, name, durable: durable, autoDelete: autoDelete, messageTTL: messageTTL, maxLength: maxLength, maxLengthBytes: maxLengthBytes, publishOnly: publishOnly); lock (syncLock) { channels.Add(channel); } return(channel); } }
//--------------------------------------------------------------------- // Static members /// <summary> /// Returns the summary from a hive proxy. /// </summary> /// <param name="hive">The target hive proxy.</param> /// <param name="definition">Optionally overrides the hive definition passed within <paramref name="hive"/>.</param> /// <returns>The <see cref="HiveSummary"/>.</returns> public static HiveSummary FromHive(HiveProxy hive, HiveDefinition definition = null) { Covenant.Requires <ArgumentNullException>(hive != null); var summary = new HiveSummary(); // Load the hive globals. var globals = hive.Consul.Client.KV.DictionaryOrEmpty(HiveConst.GlobalKey).Result; var internals = new HashSet <string>(StringComparer.InvariantCultureIgnoreCase) { // We don't include these internal globals in the summary. HiveGlobals.DefinitionDeflate, HiveGlobals.DefinitionHash, HiveGlobals.PetsDefinition }; foreach (var item in globals.Where(i => !internals.Contains(i.Key))) { summary.Globals.Add(item.Key, Encoding.UTF8.GetString(item.Value)); } // Summarize information from the hive definition. summary.NodeCount = definition.Nodes.Count(); summary.ManagerCount = definition.Managers.Count(); summary.WorkerCount = definition.Workers.Count(); summary.PetCount = definition.Pets.Count(); summary.OperatingSystem = definition.HiveNode.OperatingSystem; summary.HostingEnvironment = definition.Hosting.Environment; summary.HiveFSEnabled = definition.HiveFS.Enabled; summary.LogEnabled = definition.Log.Enabled; summary.VpnEnabled = definition.Vpn.Enabled; return(summary); }
/// <summary> /// Sets the Linux file permissions. /// </summary> /// <param name="path">Path to the target file or directory.</param> /// <param name="mode">Linux file permissions.</param> /// <param name="recursive">Optionally apply the permissions recursively.</param> public static void Set(string path, string mode, bool recursive = false) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(path)); Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(mode)); // $todo(jeff.lill): // // We're going to hack this by running [chmod MODE PATH]. Eventually, // we could convert this to using a low-level package but I didn't // want to spend time trying to figure that out right now. // // https://www.nuget.org/packages/Mono.Posix.NETStandard/1.0.0 if (!NeonHelper.IsLinux) { throw new NotSupportedException("This method requires Linux."); } object[] args; if (recursive) { args = new object[] { "-R", mode, path }; } else { args = new object[] { mode, path }; } var response = NeonHelper.ExecuteCaptureAsync("chmod", new object[] { mode, path }).Result; if (response.ExitCode != 0) { throw new IOException(response.ErrorText); } }
public void UpdateCovenantRepositoryTest() { //Arrange ICovenantRepository repository = new CovenantRepository(); Covenant Covenant = _contextForTest.Covenants.Find(1); Covenant.Name = "Teste"; Covenant.Business = "NDD"; Covenant.Plan = "0000"; Covenant.Coverage = 20; //Action var updatedCovenant = repository.Update(Covenant); //Assert var persistedCovenant = _contextForTest.Covenants.Find(1); Assert.IsNotNull(updatedCovenant); Assert.AreEqual(updatedCovenant.Id, persistedCovenant.Id); Assert.AreEqual(updatedCovenant.Name, persistedCovenant.Name); Assert.AreEqual(updatedCovenant.Business, persistedCovenant.Business); Assert.AreEqual(updatedCovenant.Plan, persistedCovenant.Plan); Assert.AreEqual(updatedCovenant.Coverage, persistedCovenant.Coverage); }
//--------------------------------------------------------------------- // Implementation /// <summary> /// Performs development related cluster checks with information on potential /// problems being written to STDOUT. /// </summary> /// <param name="clusterLogin">Specifies the target cluster login.</param> /// <param name="k8s">Specifies the cluster's Kubernertes client.</param> /// <returns><c>true</c> when there are no problems, <c>false</c> otherwise.</returns> public static async Task <bool> CheckAsync(ClusterLogin clusterLogin, IKubernetes k8s) { Covenant.Requires <ArgumentNullException>(clusterLogin != null, nameof(clusterLogin)); Covenant.Requires <ArgumentNullException>(k8s != null, nameof(k8s)); var error = false; if (!await CheckNodeContainerImagesAsync(clusterLogin, k8s)) { error = true; } if (!await CheckPodPrioritiesAsync(clusterLogin, k8s)) { error = true; } if (!await CheckResourcesAsync(clusterLogin, k8s)) { error = true; } return(error); }
/// <summary> /// Registers an activity type. /// </summary> /// <param name="client">The associated client.</param> /// <param name="activityType">The activity type.</param> /// <param name="activityTypeName">The name used to identify the implementation.</param> /// <returns><c>true</c> if the activity was already registered.</returns> /// <exception cref="InvalidOperationException">Thrown if a different activity class has already been registered for <paramref name="activityTypeName"/>.</exception> internal static bool Register(CadenceClient client, Type activityType, string activityTypeName) { Covenant.Requires <ArgumentNullException>(client != null); CadenceHelper.ValidateActivityImplementation(activityType); activityTypeName = GetActivityTypeKey(client, activityTypeName); var constructInfo = new ActivityInvokeInfo(); constructInfo.ActivityType = activityType; constructInfo.Constructor = constructInfo.ActivityType.GetConstructor(noTypeArgs); if (constructInfo.Constructor == null) { throw new ArgumentException($"Activity type [{constructInfo.ActivityType.FullName}] does not have a default constructor."); } lock (syncLock) { if (nameToInvokeInfo.TryGetValue(activityTypeName, out var existingEntry)) { if (!object.ReferenceEquals(existingEntry.ActivityType, constructInfo.ActivityType)) { throw new InvalidOperationException($"Conflicting activity type registration: Activity type [{activityType.FullName}] is already registered for workflow type name [{activityTypeName}]."); } return(true); } else { nameToInvokeInfo[activityTypeName] = constructInfo; return(false); } } }
/// <summary> /// Lists the Cadence domains. /// </summary> /// <param name="pageSize"> /// The maximum number of domains to be returned. This must be /// greater than or equal to one. /// </param> /// <param name="nextPageToken"> /// Optionally specifies an opaque token that can be used to retrieve subsequent /// pages of domains. /// </param> /// <returns>A <see cref="DomainListPage"/> with the domains.</returns> /// <remarks> /// <para> /// This method can be used to retrieve one or more pages of domain /// results. You'll pass <paramref name="pageSize"/> as the maximum number /// of domains to be returned per page. The <see cref="DomainListPage"/> /// returned will list the domains and if there are more domains waiting /// to be returned, will return token that can be used in a subsequent /// call to retrieve the next page of results. /// </para> /// <note> /// <see cref="DomainListPage.NextPageToken"/> will be set to <c>null</c> /// when there are no more result pages remaining. /// </note> /// </remarks> public async Task <DomainListPage> ListDomainsAsync(int pageSize, byte[] nextPageToken = null) { await SyncContext.Clear; Covenant.Requires <ArgumentException>(pageSize >= 1, nameof(pageSize)); EnsureNotDisposed(); var reply = (DomainListReply) await CallProxyAsync( new DomainListRequest() { PageSize = pageSize, NextPageToken = nextPageToken }); reply.ThrowOnError(); var domains = new List <DomainDescription>(reply.Domains.Count); foreach (var domain in reply.Domains) { domains.Add(domain.ToPublic()); } nextPageToken = reply.NextPageToken; if (nextPageToken != null && nextPageToken.Length == 0) { nextPageToken = null; } return(new DomainListPage() { Domains = domains, NextPageToken = nextPageToken }); }
/// <summary> /// Constructs a reverse proxy. /// </summary> /// <param name="serviceName"></param> /// <param name="localPort">The local port.</param> /// <param name="remotePort">The remote port.</param> /// <param name="@namespace"></param> /// Optionally specifies an acceptable server certificate. This can be used /// as a way to allow access for a specific self-signed certificate. Passing /// a certificate implies <paramref name="remoteTls"/><c>=true</c>. /// </param> /// <param name="clientCertificate"> /// Optionally specifies a client certificate. Passing a certificate implies /// <paramref name="remoteTls"/><c>=true</c>. /// </param> /// <param name="requestHandler">Optional request hook.</param> /// <param name="responseHandler">Optional response hook.</param> public PortForward( string serviceName, int localPort, int remotePort, string @namespace = "default") { Covenant.Requires <ArgumentException>(NetHelper.IsValidPort(localPort)); Covenant.Requires <ArgumentException>(NetHelper.IsValidPort(remotePort)); if (!NeonHelper.IsWindows) { throw new NotSupportedException($"[{nameof(PortForward)}] is supported only on Windows."); } this.serviceName = serviceName; this.localPort = localPort; this.remotePort = remotePort; this.@namespace = @namespace; this.kubectlProxyProcess = new Process(); // Create the client. KubeHelper.PortForward(serviceName, remotePort, localPort, @namespace, kubectlProxyProcess); }
/// <summary> /// Performs an HTTP <b>GET</b> using a specific <see cref="IRetryPolicy"/> and /// without ensuring that a success code was returned. /// </summary> /// <param name="retryPolicy">The retry policy or <c>null</c> to disable retries.</param> /// <param name="uri">The URI</param> /// <param name="args">The optional query arguments.</param> /// <param name="headers">The Optional HTTP headers.</param> /// <param name="cancellationToken">The optional <see cref="CancellationToken"/>.</param> /// <param name="logActivity">The optional <see cref="LogActivity"/> whose ID is to be included in the request.</param> /// <returns>The <see cref="JsonResponse"/>.</returns> /// <exception cref="SocketException">Thrown for network connectivity issues.</exception> public async Task <JsonResponse> GetUnsafeAsync( IRetryPolicy retryPolicy, string uri, ArgDictionary args = null, ArgDictionary headers = null, CancellationToken cancellationToken = default, LogActivity logActivity = default) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(uri)); retryPolicy = retryPolicy ?? NoRetryPolicy.Instance; return(await retryPolicy.InvokeAsync( async() => { var requestUri = FormatUri(uri, args); try { var client = this.HttpClient; if (client == null) { throw new ObjectDisposedException(nameof(JsonClient)); } var httpResponse = await client.GetAsync(requestUri, cancellationToken: cancellationToken, headers: headers, activity: logActivity); return new JsonResponse(requestUri, httpResponse, await httpResponse.Content.ReadAsStringAsync()); } catch (HttpRequestException e) { throw new HttpException(e, requestUri); } })); }
/// <summary> /// Scans the assembly passed looking for workflow implementations derived from /// <see cref="WorkflowBase"/> and tagged by <see cref="WorkflowAttribute"/> with /// <see cref="WorkflowAttribute.AutoRegister"/> set to <c>true</c> and registers /// them with Cadence. /// </summary> /// <param name="assembly">The target assembly.</param> /// <param name="domain">Optionally overrides the default client domain.</param> /// <returns>The tracking <see cref="Task"/>.</returns> /// <exception cref="TypeLoadException"> /// Thrown for types tagged by <see cref="WorkflowAttribute"/> that are not /// derived from <see cref="WorkflowBase"/>. /// </exception> /// <exception cref="InvalidOperationException">Thrown if one of the tagged classes conflict with an existing registration.</exception> /// <exception cref="WorkflowWorkerStartedException"> /// Thrown if a workflow worker has already been started for the client. You must /// register workflow implementations before starting workers. /// </exception> /// <remarks> /// <note> /// Be sure to register all of your workflow implementations before starting workers. /// </note> /// </remarks> public async Task RegisterAssemblyWorkflowsAsync(Assembly assembly, string domain = null) { await SyncContext.ClearAsync; Covenant.Requires <ArgumentNullException>(assembly != null, nameof(assembly)); EnsureNotDisposed(); foreach (var type in assembly.GetTypes().Where(t => t.IsClass)) { var workflowAttribute = type.GetCustomAttribute <WorkflowAttribute>(); if (workflowAttribute != null && workflowAttribute.AutoRegister) { var workflowTypeName = CadenceHelper.GetWorkflowTypeName(type, workflowAttribute); await WorkflowBase.RegisterAsync(this, type, workflowTypeName, ResolveDomain(domain)); lock (registeredWorkflowTypes) { registeredWorkflowTypes.Add(CadenceHelper.GetWorkflowInterface(type)); } } } }
/// <summary> /// Checks the argument passed for wildcards and expands them into the /// appopriate set of matching file names. /// </summary> /// <param name="path">The file path potentially including wildcards.</param> /// <returns>The set of matching file names.</returns> public static string[] ExpandWildcards(string path) { Covenant.Requires <ArgumentNullException>(path != null); int pos; string dir; string pattern; if (path.IndexOfAny(NeonHelper.FileWildcards) == -1) { return(new string[] { path }); } pos = path.LastIndexOfAny(new char[] { '\\', '/', ':' }); if (pos == -1) { return(Directory.GetFiles(".", path)); } dir = path.Substring(0, pos); pattern = path.Substring(pos + 1); return(Directory.GetFiles(dir, pattern)); }
/// <summary> /// Returns the value associated with a command line option if the option was present /// on the command line otherwise, the specified default value will be returned. /// </summary> /// <param name="optionName">The case sensitive option name (including the leading dashes (<b>-</b>).</param> /// <param name="def">The default value.</param> /// <returns>The option value if present, the specified default value otherwise.</returns> /// <remarks> /// <para> /// If the <paramref name="optionName"/> was included in a previous <see cref="DefineOption"/> /// call, then all aliases for the option will be searched. If the option is not /// present on the command line and <paramref name="def"/> is <c>null</c>, then the default /// defined default value will be returned otherwise <paramref name="def"/> will override /// the definition. /// </para> /// </remarks> public string GetOption(string optionName, string def = null) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(optionName)); OptionDefinition definition; string value; if (optionDefinitions.TryGetValue(optionName, out definition)) { foreach (var name in definition.Names) { if (options.TryGetValue(name, out value) && !string.IsNullOrEmpty(value)) { return(value); } } if (def != null) { return(def); } else { return(definition.Default); } } else { if (options.TryGetValue(optionName, out value)) { return(value); } return(def); } }
/// <summary> /// Transmits a signal to an external workflow, starting the workflow if it's not currently running. /// This low-level method accepts a byte array with the already encoded parameters. /// </summary> /// <param name="workflowTypeName">The target workflow type name.</param> /// <param name="signalName">Identifies the signal.</param> /// <param name="signalArgs">Optionally specifies the signal arguments as a byte array.</param> /// <param name="startArgs">Optionally specifies the workflow arguments.</param> /// <param name="options">Optionally specifies the options to be used for starting the workflow when required.</param> /// <returns>The <see cref="WorkflowExecution"/>.</returns> /// <exception cref="EntityNotExistsException">Thrown if the domain does not exist.</exception> /// <exception cref="BadRequestException">Thrown if the request is invalid.</exception> /// <exception cref="InternalServiceException">Thrown for internal Cadence problems.</exception> internal async Task <WorkflowExecution> SignalWorkflowWithStartAsync(string workflowTypeName, string signalName, byte[] signalArgs, byte[] startArgs, WorkflowOptions options) { Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(workflowTypeName), nameof(workflowTypeName)); Covenant.Requires <ArgumentNullException>(!string.IsNullOrEmpty(signalName), nameof(signalName)); EnsureNotDisposed(); options = WorkflowOptions.Normalize(this, options); var reply = (WorkflowSignalWithStartReply) await CallProxyAsync( new WorkflowSignalWithStartRequest() { Workflow = workflowTypeName, WorkflowId = options.WorkflowId, Options = options.ToInternal(), SignalName = signalName, SignalArgs = signalArgs, WorkflowArgs = startArgs, Domain = options.Domain }); reply.ThrowOnError(); return(reply.Execution.ToPublic()); }