/// <summary> /// All disk storage for this application is pointed to this directory. /// </summary> /// <param name="settings"></param> /// <returns></returns> protected string GetStoragePath(DiskServiceSettings settings) { var storage = this.GlobalSettings.GetDefaultContentStorage(); // We can have an app_setting configuration // to route a whole application to a specific sql server string diskTarget; if (this.Deployment.installedApplicationSettings.configuration["disktarget"] != null) { diskTarget = Convert.ToString(this.Deployment.installedApplicationSettings.configuration["disktarget"]); this.Logger.LogInfo(true, "Custom disk target: " + diskTarget); if (!Directory.Exists(diskTarget)) { throw new Exception("Invalid custom disk target: " + diskTarget); } } else { // Generate a unique "virtual disk" (directory) for this application diskTarget = UtilsSystem.EnsureDirectoryExists(UtilsSystem.CombinePaths( storage.path, "store_" + this.Deployment.installedApplicationSettings.GetId())); } return(diskTarget); }
/// <summary> /// Grab from a settings file. /// </summary> /// <param name="path"></param> /// <param name="logger"></param> public void PopulateFromSettingsFile(string path, ILoggerInterface logger) { string file = UtilsSystem.CombinePaths(path, "artifact-settings.yml"); if (!File.Exists(file)) { return; } var configfile = new Configuration.YamlConfigurationFile(); try { // This file might be malformed, do not crash and let other // environment information sources have their chance configfile.ParseFromFile(file); } catch (Exception e) { logger.LogException(new Exception("Error parsing file: " + file, e)); return; } // Parse the artifact settings... this.branch = configfile.GetStringValue("repo-branch", null); this.commit_sha = configfile.GetStringValue("repo-commit", null); this.version = configfile.GetStringValue("build-id", null); }
/// <summary> /// Get a deployer for the installed application. /// </summary> /// <param name="globalSettings">The global settings.</param> /// <param name="installedApplicationSettings">The installed application settings.</param> /// <param name="logger">The logger.</param> public ApplicationDeployer( EnvironmentSettings globalSettings, InstalledApplication installedApplicationSettings, ILoggerInterface logger) { this.GlobalSettings = globalSettings; this.installedAppSettings = installedApplicationSettings; this.Logger = logger; if (this.GlobalSettings == null) { throw new InvalidDataException("settings argument cannot be null."); } if (this.installedAppSettings == null) { throw new Exception("installedApplicationSettings argument cannot be null."); } // Try to grab previous deployment... this.activeDeploymentPathStorage = UtilsSystem.CombinePaths(globalSettings.activeDeploymentDir, "active." + this.installedAppSettings.GetId() + ".json"); if (File.Exists(this.activeDeploymentPathStorage)) { this.DeploymentActive = Deployment.InstanceFromPath(this.activeDeploymentPathStorage, globalSettings); } }
public override void beforeDone() { // We also have a canonical access to the deployed app through a symlink string basePath = this.Deployment.GetSetting("appstorage.base", (string)null, this.Logger); string canonicalPath = UtilsSystem.CombinePaths(this.GlobalSettings.GetDefaultApplicationStorage().path, "_" + this.Deployment.installedApplicationSettings.GetId()); UtilsJunction.EnsureLink(canonicalPath, basePath, this.Logger, false, true); this.Deployment.SetSetting("appstorage.canonical", canonicalPath); }
/// <summary> /// /// </summary> /// <param name="filename"></param> /// <returns></returns> protected string GetGlobalStoragePath(string filename) { var environmentSettingsFile = UtilsSystem.EnsureDirectoryExists( UtilsSystem.CombinePaths( Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "iischef", "config", filename)); return(environmentSettingsFile); }
/// <summary> /// Get an instance of AppVeyorClient /// </summary> /// <param name="token">API Token</param> /// <param name="baseUri">Base URI</param> /// <param name="logger"></param> /// <param name="tempDir"></param> public Client( string token, string baseUri, ILoggerInterface logger, string tempDir) { string apiTempDir = UtilsSystem.EnsureDirectoryExists(UtilsSystem.CombinePaths(tempDir, "_appveyor", "api"), true); this.TempDir = tempDir; this.Token = token; this.Logger = logger; this.BaseUri = baseUri; this.SimpleStore = new SimpleStore(apiTempDir); }
/// <summary> /// Find the matching deployer of the parent application when /// inheritance is configured for this application. /// </summary> /// <typeparam name="TType"></typeparam> /// <returns></returns> public TType getDeployerFromParentApp <TType>() where TType : DeployerBase { // We need a parent application for this to work. if (this.ParentApp == null) { return(null); } // Try to grab parent deployment... Deployment parentDeployment; string activeDeploymentPathStorage = UtilsSystem.CombinePaths(this.GlobalSettings.activeDeploymentDir, "active." + this.ParentApp.GetId() + ".json"); if (File.Exists(activeDeploymentPathStorage)) { parentDeployment = Deployment.InstanceFromPath(activeDeploymentPathStorage, this.GlobalSettings); DeployerSettingsBase ourSettings = this.DeployerSettings.castTo <DeployerSettingsBase>(); List <IDeployerInterface> deployersAndServices = new List <IDeployerInterface>(); deployersAndServices.AddRange(parentDeployment.GrabServices(this.Logger)); deployersAndServices.AddRange(parentDeployment.GrabDeployers(this.Logger)); // Only keep those that match our type deployersAndServices = deployersAndServices.Where(s => s.GetType() == typeof(TType)).ToList(); // Filter by ID foreach (TType t in deployersAndServices) { if (t.DeployerSettings.castTo <DeployerSettingsBase>().id == ourSettings.id) { return(t); } } return(null); } else { return(null); } }
/// <summary> /// Grab from local GIT repo. /// </summary> /// <param name="path"></param> public void PopulateFromGit(string path) { // Crawl up to find the first directory covered by GIT. There might be a difference // between the artifact folder structure and the repository (local working copy) itself... DirectoryInfo difo = new DirectoryInfo(path); string gitpath = null; while (difo != null && difo.Exists) { if (Directory.Exists(UtilsSystem.CombinePaths(difo.FullName, ".git"))) { gitpath = difo.FullName; break; } difo = difo.Parent; } if (gitpath == null) { return; } try { // Try to get information directly from GIT?? var repo = new LibGit2Sharp.Repository(gitpath, new LibGit2Sharp.RepositoryOptions() { }); this.branch = repo.Head.FriendlyName; this.commit_sha = repo.Commits.First().Sha; this.version = this.commit_sha; } catch (Exception e) { // Trying to read settings from GIT can be delicate. Such as... // https://github.com/GitTools/GitVersion/issues/1043 } }
/// <summary> /// Get the webroot for the CDN site, initialized with a base web.config prepared for URL REWRITING /// </summary> /// <returns></returns> public string GetCdnWebConfigPathInitialized() { var basedir = UtilsSystem.EnsureDirectoryExists( UtilsSystem.CombinePaths(this.GlobalSettings.GetDefaultApplicationStorage().path, "__chef_cdn"), true); var webconfigfilepath = UtilsSystem.CombinePaths(basedir, "web.config"); // Si no hay un web.config plantilla, crearlo ahora. if (!File.Exists(webconfigfilepath)) { File.WriteAllText(webconfigfilepath, @" <configuration> <system.webServer> <rewrite> <rules> </rules> <outboundRules> </outboundRules> </rewrite> </system.webServer> </configuration> "); } UtilsWindowsAccounts.AddPermissionToDirectoryIfMissing(new SecurityIdentifier(UtilsWindowsAccounts.WELL_KNOWN_SID_USERS), basedir, FileSystemRights.ReadAndExecute); // Make sure that the site exists using (ServerManager manager = new ServerManager()) { bool configChanged = false; var site = UtilsIis.FindSiteWithName(manager, this.CstChefCndSiteName, this.Logger) .FirstOrDefault(); if (site == null) { manager.Sites.Add(this.CstChefCndSiteName, "http", $"{UtilsIis.LOCALHOST_ADDRESS}:80:{this.CstChefInternalHostname}", basedir); configChanged = true; } else { if (site.Applications.First().VirtualDirectories.First().PhysicalPath != basedir) { site.Applications.First().VirtualDirectories.First().PhysicalPath = basedir; configChanged = true; } } if (configChanged) { UtilsIis.CommitChanges(manager); } } this.UtilsHosts.AddHostsMapping(UtilsIis.LOCALHOST_ADDRESS, this.CstChefInternalHostname, "chf_IISDeployer_CDN"); // Add a cross domain file var crossdomainfilepath = UtilsSystem.CombinePaths(Path.GetDirectoryName(webconfigfilepath), "crossdomain.xml"); File.WriteAllText(crossdomainfilepath, UtilsSystem.GetEmbededResourceAsString(Assembly.GetExecutingAssembly(), "IIS.crossdomain.xml")); // Add common proxy headers UtilsIis.AddAllowedServerVariablesForUrlRewrite( this.CstChefCndSiteName, "HTTP_X_FORWARDED_FOR", "HTTP_X_FORWARDED_PROTO", "HTTP_X_FORWARDED_HOST"); return(webconfigfilepath); }
/// <inheritdoc cref="DeployerInterface"/> public void deploy() { var settings = this.Settings; string cronId = this.Deployment.shortid + "_" + settings.id; string pwfile = UtilsSystem.CombinePaths(this.Deployment.runtimePath, "cronjobs_" + settings.id + ".ps1"); // Necesitamos un Bat que llame al powershel, este siempre tiene el mismo aspecto. string batfile = UtilsSystem.CombinePaths(this.Deployment.runtimePath, "cronjobs_" + settings.id + ".bat"); Encoding enc = Encoding.GetEncoding("Windows-1252"); File.WriteAllText(batfile, "powershell " + pwfile, enc); StringBuilder command = new StringBuilder(); // Add path to environment. command.AppendLine( $"$env:Path = \"{UtilsSystem.CombinePaths(this.Deployment.runtimePath, "include_path")};\" + $env:Path"); // Move to runtime. command.AppendLine($"cd \"{UtilsSystem.CombinePaths(this.Deployment.appPath)}\""); // Add path of project to the enviroment command.AppendLine($"$env:AppPath = \"{UtilsSystem.CombinePaths(this.Deployment.appPath)}\""); // Whatever deployers wanna do... var logger = new logger.NullLogger(); var deployers = this.Deployment.GrabDeployers(logger); foreach (var deployer in deployers) { deployer.deployConsoleEnvironment(command); } // Drop the user commands if (!string.IsNullOrWhiteSpace(settings.command)) { command.AppendLine(settings.command); } if (settings.commands != null) { foreach (var cmd in settings.commands) { command.AppendLine(cmd); } } File.WriteAllText(pwfile, command.ToString()); // Nuestro scheduler tiene un nombre // definido. using (TaskService ts = new TaskService()) { // Create a new task definition and assign properties TaskDefinition td = ts.NewTask(); // Run with highest level to avoid UAC issues // https://www.devopsonwindows.com/create-scheduled-task/ td.Principal.RunLevel = TaskRunLevel.Highest; string password = settings.taskUserPassword; if (settings.taskLogonType.HasValue) { td.Principal.LogonType = (TaskLogonType)settings.taskLogonType.Value; } if (settings.taskUserId == "auto") { td.Principal.UserId = this.Deployment.WindowsUsernameFqdn(); td.Principal.LogonType = TaskLogonType.Password; password = this.Deployment.GetWindowsPassword(); // Make sure that the user has the LogonAsBatchRight UtilsWindowsAccounts.SetRight(this.Deployment.WindowsUsernameFqdn(), "SeBatchLogonRight", logger); } // Default to the SYSTEM account. else if (string.IsNullOrWhiteSpace(settings.taskUserId)) { td.Principal.UserId = "SYSTEM"; td.Principal.LogonType = TaskLogonType.ServiceAccount; password = null; } td.RegistrationInfo.Description = cronId; // Create a trigger that will fire the task every 5 minutes. var trigger = new DailyTrigger(); // Habilitada... trigger.Enabled = true; // Repetir cada 24 horas. trigger.DaysInterval = 1; // Repetir durante 24 horas en la frecuencia establecida. trigger.Repetition = new RepetitionPattern(new TimeSpan(0, settings.frequency, 0), new TimeSpan(24, 0, 0), true); // Para que arranque dos minutos después del deploy. trigger.StartBoundary = DateTime.Now.AddMinutes(2); // Enablin/disabling will happen during start/stop of service td.Settings.Enabled = false; // Un solo trigger. td.Triggers.Add(trigger); // Create an action that will launch the bat launcher. td.Actions.Add(new ExecAction(batfile, null, null)); TaskFolder f = this.GetFolder(ts); // Register the task in the root folder if (!string.IsNullOrWhiteSpace(password) && td.Principal.LogonType == TaskLogonType.Password) { f.RegisterTaskDefinition(td.RegistrationInfo.Description, td, TaskCreation.Create, td.Principal.UserId, this.Deployment.GetWindowsPassword(), td.Principal.LogonType); } else { f.RegisterTaskDefinition(td.RegistrationInfo.Description, td, TaskCreation.Create, td.Principal.UserId); } } }
/// <summary> /// A local writable temporary directory /// </summary> /// <returns></returns> protected string GetSysTempDir() { return(UtilsSystem.EnsureDirectoryExists(UtilsSystem.CombinePaths(this.Deployment.tempPathSys, "sys_temp_dir"), true)); }
/// <summary> /// Deploys an installed app. /// </summary> /// <param name="app"></param> /// <param name="force"></param> /// <param name="buildId"></param> /// <param name="sync"></param> protected Deployment _DeployApp( Application app, bool force = false, string buildId = null, bool sync = false) { DateTime start = DateTime.Now; // The parent application to inherit from (if needed) InstalledApplication parentApp = null; // Lo primero es ver si hay algo nuevo... var downloader = this.installedAppSettings.GetDownloader(this.GlobalSettings, this.Logger); if (!string.IsNullOrWhiteSpace(buildId)) { this.Logger.LogInfo(true, "Deploying specific version build: '{0}'", buildId); } string nextArtifactId; // Next artifact id might be pulled from a remote location, and this prompt to random failures (network, etc.) // so wrap this in a try/catch try { nextArtifactId = downloader.GetNextId(buildId == "latest" ? null : buildId); } catch (Exception e) { this.Logger.LogException(new Exception("Failure while looking for next build ID", e), EventLogEntryType.Warning); return(this.DeploymentActive); } string currentArtifactId = this.DeploymentActive != null ? this.DeploymentActive.artifact.id : string.Empty; bool isNew = this.DeploymentActive == null || this.DeploymentActive.artifact.id != nextArtifactId; // Check that Inherit application exists if (!string.IsNullOrEmpty(this.installedAppSettings.GetInherit())) { parentApp = app.GetInstalledApp(this.installedAppSettings.GetInherit()); if (parentApp == null) { throw new Exception( $"Application from inheritation: {this.installedAppSettings.GetInherit()}, can not be found"); } this.Logger.LogInfo(true, "Application configured to inherit from parent application '{0}'. Sync:{1}", parentApp.GetId(), sync ? "Yes" : "No"); } // Si no es nuevo y no estamos forzando, no hacer deploy. if (!isNew && !force) { this.Logger.LogInfo(true, "No new version found for Application {0}", this.installedAppSettings.GetId()); return(this.DeploymentActive); } // There is an existing deployment that had a manually enforced BuildId if (!string.IsNullOrEmpty(this.DeploymentActive?.enforceBuildId)) { if (force) { if (!string.IsNullOrWhiteSpace(buildId)) { // If there is a force and a buildId has been specified, override the next artifactId // with the requested BuildId, or if latest was specified use that. if (buildId != "latest") { nextArtifactId = buildId; } } else { // If no specific build was requested, override the nextArtifactId with // the stored build nextArtifactId = this.DeploymentActive.enforceBuildId; this.Logger.LogWarning(true, "Deploying stored version {0}", nextArtifactId); } } else if (buildId != this.DeploymentActive.enforceBuildId && buildId != "latest") { this.Logger.LogWarning(true, $"Deployment was skipped because previous deployment was a version-specific deployment. Previous buildId='{this.DeploymentActive.enforceBuildId}'. Requested buildId='{buildId}'. Use buildId='latest' to force deploying the latest succesful build or -Force to deploy this version."); return(this.DeploymentActive); } } this.Logger.LogInfo(false, "@@ Starting deployment for app: '{0}'", this.installedAppSettings.GetId()); this.Logger.LogInfo(false, "Current artifact: '{0}' || Previous artifact: '{1}'", nextArtifactId, currentArtifactId); // Specify a local temporary artifact location, in case this is supported by the downloader... // final path should be retrieved from artifact.localPath string preferredLocalArtifactPath = UtilsSystem.EnsureDirectoryExists( UtilsSystem.CombinePaths( this.GlobalSettings.GetDefaultApplicationStorage().path, "_tmp", this.installedAppSettings.GetId(), UtilsEncryption.GetShortHash(nextArtifactId, 12)), true); // Get from the ID... Artifact artifact = downloader.PullFromId(nextArtifactId, preferredLocalArtifactPath); if (string.IsNullOrWhiteSpace(this.GlobalSettings.id)) { throw new Exception("Environment settings cannot have an empty ID."); } this.Logger.LogInfo(false, "Environment id: '{0}'", this.GlobalSettings.id); this.Logger.LogInfo(false, "Environment options/tags: '{0}'", string.Join(",", this.GlobalSettings.getOptions())); this.Logger.LogInfo(false, "Pull artifact lapsed: {0}s", (DateTime.Now - start).TotalSeconds); start = DateTime.Now; // Look for a configuration file that fits this environment. string chefsettingsdir = UtilsSystem.CombinePaths(artifact.localPath, "chef"); // The final chef configuration files is a combination of Chef files var appSettings = this.LoadApplicationSettings( chefsettingsdir, this.GlobalSettings.id, artifact.artifactSettings.branch, out var loadedConfigurationFiles); // Storage for current deployment. Includes all possible environment data // in order to provide traceability + rollback capabilities. Deployment deployment = new Deployment( appSettings, this.GlobalSettings, artifact, this.installedAppSettings, parentApp); deployment.SetPreviousDeployment(this.DeploymentActive); // Check the deployment windows! var deploymentSettings = deployment.appSettings.getDeploymentSettings(); if (deploymentSettings != null) { if (deploymentSettings.deployment_windows != null && deploymentSettings.deployment_windows.Any()) { bool canDeploy = false; foreach (var deploymentWindow in deploymentSettings.deployment_windows) { TimeZoneInfo info = TimeZoneInfo.FindSystemTimeZoneById(deploymentWindow.Value.timezone); TimeSpan dtStart = TimeSpan.Parse(deploymentWindow.Value.start); TimeSpan dtEnd = TimeSpan.Parse(deploymentWindow.Value.end); DateTimeOffset localServerTime = DateTimeOffset.Now; DateTimeOffset windowTimeZone = TimeZoneInfo.ConvertTime(localServerTime, info); TimeSpan dtNow = windowTimeZone.TimeOfDay; if (dtStart <= dtEnd) { // start and stop times are in the same day if (dtNow >= dtStart && dtNow <= dtEnd) { // current time is between start and stop canDeploy = true; break; } } else { // start and stop times are in different days if (dtNow >= dtStart || dtNow <= dtEnd) { // current time is between start and stop canDeploy = true; break; } } } // Even if we are not in a deployment windows, // if we are forcing the deployment continue. if (!canDeploy && !force) { this.Logger.LogInfo(false, "Application deployment skipped. Current time not within allowed publishing windows."); return(this.DeploymentActive); } } } // Inform about the confiugration files that where used for loading deployment.SetRuntimeSetting("deployment.loaded_configuration_files", string.Join(",", loadedConfigurationFiles)); deployment.enforceBuildId = buildId == "latest" ? null : buildId; var deployersActive = this.DeploymentActive != null?this.DeploymentActive.GrabDeployers(this.Logger) : new DeployerCollection(this.GlobalSettings, null, this.Logger, parentApp); var servicesActive = this.DeploymentActive != null?this.DeploymentActive.GrabServices(this.Logger) : new DeployerCollection(this.GlobalSettings, null, this.Logger, parentApp); var deployers = deployment.GrabDeployers(this.Logger); var services = deployment.GrabServices(this.Logger); this.Logger.LogInfo(false, "Deployers and services gathered. Starting installation..."); var settingsConverter = new JObjectToKeyValueConverter(); try { // Deploy the application base storage (logs, runtime, etc.) deployers.DeployAll(); services.DeployAll(); // Move the application settings to runtime settings var userApplicationSettings = appSettings.getApplicationSettings(); foreach (var k in settingsConverter.NestedToKeyValue(userApplicationSettings)) { deployment.SetRuntimeSetting("app_settings." + k.Key, k.Value); } // Sync if (sync) { deployers.SyncAll(); services.SyncAll(); } // Time to hot switch the sites... we need to waitPauseMs for all // current requests to finish... because that way we ensure // that underlying storage updates will not collide if updates // are being deployed. servicesActive.StopAll(); deployersActive.StopAll(); // Some stuff requires the old services to be stopped in order to be deployed, such as IIS bindings and certificates deployers.BeforeDoneAll(); services.BeforeDoneAll(); var settingsToDeploy = deployment.GetRuntimeSettingsToDeploy(); // Store Key-Value settings in a JSON object (with keys as var jsonSettings = JsonConvert.SerializeObject( settingsToDeploy, Formatting.Indented); var jsonSettingsNested = JsonConvert.SerializeObject( settingsConverter.keyValueToNested(settingsToDeploy), Formatting.Indented); // Make sure we persist the settings AFTER all deployers have finished thri job deployers.DeploySettingsAll(jsonSettings, jsonSettingsNested); services.DeploySettingsAll(jsonSettings, jsonSettingsNested); // Time to start! deployers.StartAll(); services.StartAll(); DateTime dtStart = DateTime.Now; // Replace active configuration settings deployment.StoreInPath(this.activeDeploymentPathStorage); // Quitar el deployment anterior y si hay error seguir, // ya que los datos del deployment actual YA están guardados! servicesActive.UndeployAll(true); deployersActive.UndeployAll(true); // The done "event" is called on deployers // once everything is completed correctly. deployers.DoneAll(true); services.DoneAll(true); // Make sure that at least 2 seconds pass after deployment before // doing an OK to let IIS reconfigure. while ((DateTime.Now - dtStart).TotalSeconds < 1) { System.Threading.Thread.Sleep(500); } } catch (Exception e) { // Just in case.... log this ASAP this.Logger.LogException( new Exception("Error deploying APP: " + deployment.installedApplicationSettings.GetId(), e)); deployers.StopAll(true); deployers.UndeployAll(true); // Aquí hacemos un continue on error porque... estamos repescando algo que ya funcionaba // a toda costa queremos levantarlo! deployersActive.StartAll(true); servicesActive.StopAll(true); // In unit test rethrow to preserve stack trace in GUI if (UnitTestDetector.IsRunningInTests) { System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(e) .Throw(); } else { throw new AlreadyHandledException(e.Message, e); } } finally { // Run cleanup, dot not fail if cleanup fails, it's just an extra... deployers.CleanupAll(true); services.CleanupAll(true); } // Done! this.Logger.LogInfo(false, "Deployment lapsed: {0}s", (DateTime.Now - start).TotalSeconds); return(deployment); }
public override void _sync(object input) { var sqlSettings = this.DeployerSettings.castTo <SQLServiceSettings>(); SQLService parent = (SQLService)input; string database = this.Deployment.GetRuntimeSettingsToDeploy()["services." + this.DeployerSettings.castTo <SQLServiceSettings>().id + ".database"]; string parentDatabase = parent.Deployment.GetRuntimeSettingsToDeploy()["services." + parent.DeployerSettings.castTo <SQLServiceSettings>().id + ".database"]; var sqlServer = this.GetSqlServer(sqlSettings.id); using (SqlConnection connection = new SqlConnection(sqlServer.connectionString)) { connection.Open(); ServerConnection serv = new ServerConnection(connection); smo.Server serverTemp = new smo.Server(serv); string backupDir = serverTemp.BackupDirectory; string dataDir = serverTemp.MasterDBPath; this.Logger.LogInfo(true, "SQL Server Version: " + serverTemp.VersionString); this.Logger.LogInfo(true, "SQL Server Edition: " + serverTemp.Edition); var backupName = database + DateTime.Now.ToString("yyyyMMddHHmmssffff"); var backupFile = UtilsSystem.CombinePaths(backupDir, backupName + ".bak"); SqlCommand cmd; // Timeout for long running processes (i.e. backup and restore) int longProcessTimeout = 120; try { string query = null; // EngineEdition Database Engine edition of the instance of SQL Server installed on the server. // 1 = Personal or Desktop Engine(Not available in SQL Server 2005 and later versions.) // 2 = Standard(This is returned for Standard, Web, and Business Intelligence.) // 3 = Enterprise(This is returned for Evaluation, Developer, and both Enterprise editions.) // 4 = Express(This is returned for Express, Express with Tools and Express with Advanced Services) // 5 = SQL Database // 6 - SQL Data Warehouse bool supportsCompression = serverTemp.EngineEdition != smo.Edition.Express; string compressionOption = supportsCompression ? "COMPRESSION," : string.Empty; query = string.Format( "BACKUP DATABASE [{0}] to disk = '{1}' WITH {3} name = '{2}'", parentDatabase, backupFile, backupName, compressionOption); this.Logger.LogInfo(true, "CMD: {0}", query); cmd = new SqlCommand(query, connection); cmd.CommandTimeout = longProcessTimeout; cmd.ExecuteNonQuery(); query = string.Format( @"DECLARE @kill varchar(8000) = ''; SELECT @kill = @kill + 'kill ' + CONVERT(varchar(5), session_id) + ';' FROM sys.dm_exec_sessions WHERE database_id = db_id('{0}')", database); this.Logger.LogInfo(true, "CMD: {0}", query); cmd = new SqlCommand(query, connection); cmd.ExecuteNonQuery(); query = string.Format("ALTER DATABASE [{0}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE", database); this.Logger.LogInfo(true, "CMD: {0}", query); cmd = new SqlCommand(query, connection); cmd.ExecuteNonQuery(); query = string.Format("RESTORE filelistonly FROM disk='{0}'", backupFile); // Before restoring, print out logical file names cmd = new SqlCommand(query, connection); Dictionary <string, string> info = new Dictionary <string, string>(); using (var reader = cmd.ExecuteReader()) { int row = 0; while (reader.Read()) { for (var i = 0; i < reader.FieldCount; i++) { var columname = reader.GetName(i); var columnvalue = Convert.ToString(reader[i]); info.Add(row.ToString() + "_" + columname, columnvalue); } row++; } } // logger.LogInfo(true, "BACKUP DETAILS: {0}", Newtonsoft.Json.JsonConvert.SerializeObject(info, Newtonsoft.Json.Formatting.Indented)); Dictionary <string, string> dataFilesToMove = new Dictionary <string, string>(); for (int x = 0; x < 500; x++) { if (!info.ContainsKey($"{x}_LogicalName")) { break; } var logicalName = info[$"{x}_LogicalName"]; var physicalName = info[$"{x}_PhysicalName"]; var fileName = System.IO.Path.GetFileName(physicalName); dataFilesToMove.Add(logicalName, $"{dataDir}\\2_{fileName}"); } List <string> fileMoves = new List <string>(); foreach (var dataFile in dataFilesToMove) { fileMoves.Add($"MOVE '{dataFile.Key}' TO '{dataFile.Value}'"); } query = string.Format($"RESTORE DATABASE [{database}] FROM DISK = '{backupFile}' WITH REPLACE, {string.Join(",", fileMoves)};"); this.Logger.LogInfo(true, "CMD: {0}", query); cmd = new SqlCommand(query, connection); cmd.CommandTimeout = longProcessTimeout; cmd.ExecuteNonQuery(); query = $"ALTER DATABASE [{database}] SET MULTI_USER"; this.Logger.LogInfo(true, "CMD: {0}", query); cmd = new SqlCommand(query, connection); cmd.ExecuteNonQuery(); serverTemp.ConnectionContext.ExecuteNonQuery("EXEC sp_configure 'show advanced options', 1"); serverTemp.ConnectionContext.ExecuteNonQuery("RECONFIGURE"); serverTemp.ConnectionContext.ExecuteNonQuery("EXEC sp_configure 'xp_cmdshell', 1"); serverTemp.ConnectionContext.ExecuteNonQuery("RECONFIGURE"); serverTemp.ConnectionContext.ExecuteNonQuery($"xp_cmdshell 'del \"{backupFile}\"'"); serverTemp.ConnectionContext.ExecuteNonQuery("EXEC sp_configure 'xp_cmdshell', 0"); serverTemp.ConnectionContext.ExecuteNonQuery("EXEC sp_configure 'show advanced options', 0"); serverTemp.ConnectionContext.ExecuteNonQuery("RECONFIGURE"); } finally { // Make sure that we remove single_user_mode try { string query = null; query = $"ALTER DATABASE [{database}] SET MULTI_USER"; cmd = new SqlCommand(query, connection); cmd.ExecuteNonQuery(); } catch { // ignored } } connection.Close(); } // After doing the SYNC we need to "re-deploy" the database so that // user accounts are properly setup for the new application. this.deploy(); }
/// <summary> /// A file name that will be used for physical file locks /// </summary> /// <returns></returns> protected string LockPathForApplication() { return(UtilsSystem.EnsureDirectoryExists( UtilsSystem.CombinePaths(this.GlobalSettings.GetDefaultApplicationStorage().path, "_chef_locks", this.GlobalSettings.id, "application." + this.installedAppSettings.GetId() + ".lock"))); }
/// <summary> /// Path to the environment settings folder /// </summary> /// <param name="settingsFile"></param> public void Initialize(string settingsFile = null, string options = null) { var environmentSettingsFile = this.GetGlobalStorageVariable("environment-file-path"); if (settingsFile == null && !File.Exists(environmentSettingsFile)) { throw new Exception("To start the deployer you need to provide a valid environment configuration file. The default location is: " + environmentSettingsFile); } if (settingsFile != null) { environmentSettingsFile = settingsFile; } var serverSettingsContent = File.ReadAllText(environmentSettingsFile); this.GlobalSettings = JsonConvert.DeserializeObject <EnvironmentSettings>(serverSettingsContent); // Ensure we have a salt if (string.IsNullOrWhiteSpace(this.GlobalSettings.installationSalt)) { this.Logger.LogWarning(true, "Global parameter 'installationSalt' no defined, using default salt."); this.GlobalSettings.installationSalt = "default-salt"; } // Initialize the settings directory if (string.IsNullOrEmpty(this.GlobalSettings.settingsDir)) { this.GlobalSettings.settingsDir = Path.GetDirectoryName(environmentSettingsFile); this.Logger.LogInfo(true, "No 'settingsDir' directory specified. Using default: {0}", environmentSettingsFile); } // Active deployment directory if (string.IsNullOrWhiteSpace(this.GlobalSettings.activeDeploymentDir)) { this.GlobalSettings.activeDeploymentDir = UtilsSystem.CombinePaths(this.GlobalSettings.settingsDir, "deployments"); // Initialize storage if (!Directory.Exists(this.GlobalSettings.activeDeploymentDir)) { Directory.CreateDirectory(this.GlobalSettings.activeDeploymentDir); } this.Logger.LogInfo(true, "No 'activeDeploymentDir' directory specified. Using default: {0}", this.GlobalSettings.activeDeploymentDir); } // Template directory if (string.IsNullOrWhiteSpace(this.GlobalSettings.applicationTemplateDir)) { this.GlobalSettings.applicationTemplateDir = UtilsSystem.CombinePaths(this.GlobalSettings.settingsDir, "installed_apps"); // Initialize storage if (!Directory.Exists(this.GlobalSettings.applicationTemplateDir)) { Directory.CreateDirectory(this.GlobalSettings.applicationTemplateDir); } this.Logger.LogInfo(true, "No 'applicationTemplateDir' directory specified. Using default: {0}", this.GlobalSettings.applicationTemplateDir); } if (this.GlobalSettings.options == null) { this.GlobalSettings.options = new List <string>(); } if (options != null) { foreach (var option in options.Split(",".ToCharArray())) { if (!this.GlobalSettings.options.Contains(option)) { this.GlobalSettings.options.Add(option); } } } // Now move to a file based logger // and keep track of original logger. this.parentLogger = this.Logger; this.Logger = new FileLogger(UtilsSystem.CombinePaths(this.GlobalSettings.GetDefaultLogStorage().path, $"chef-application-{this.GlobalSettings.id}.log")); }
/// <summary> /// Deploy the application runtime settings. /// </summary> /// <param name="jsonSettings"></param> /// <param name="jsonSettingsArray"></param> /// <param name="replacer"></param> public void deploySettings( string jsonSettings, string jsonSettingsArray, RuntimeSettingsReplacer replacer) { // Write the settings in a directory in the application folder itself. // When deployed as web app or similar, the other deployers must hide this directory... var settingsFile = UtilsSystem.EnsureDirectoryExists( Path.Combine(this.Deployment.runtimePath, "chef-settings.json")); File.WriteAllText(settingsFile, jsonSettings); var settingsFileNested = UtilsSystem.EnsureDirectoryExists( Path.Combine(this.Deployment.runtimePath, "chef-settings-nested.json")); File.WriteAllText(settingsFileNested, jsonSettingsArray); // Why don't we write the settings directly to the AppRoot? Because it might // be exposed to the public if the application is mounted as a web application... // So we just hint to the location of the runtime, and the application // must implement the code needed to load the settings. var hintFile = UtilsSystem.EnsureDirectoryExists( UtilsSystem.CombinePaths(this.Deployment.appPath, "chef-runtime.path")); // We hint to the runtime path, not the specific file File.WriteAllText(hintFile, this.Deployment.runtimePath); // Dump the configuration files if requested to do so... foreach (var kvp in this.GetSettings().configuration_dump_paths ?? new Dictionary <string, string>()) { var destinationDir = UtilsSystem.CombinePaths(this.Deployment.appPath, kvp.Value); if (!Directory.Exists(destinationDir)) { Directory.CreateDirectory(destinationDir); } var settingsFileDump = UtilsSystem.EnsureDirectoryExists( Path.Combine(destinationDir, "chef-settings.json")); File.WriteAllText(settingsFileDump, jsonSettings); var settingsFileNestedDump = UtilsSystem.EnsureDirectoryExists( Path.Combine(destinationDir, "chef-settings-nested.json")); File.WriteAllText(settingsFileNestedDump, jsonSettingsArray); var settingsFileNestedYaml = UtilsSystem.EnsureDirectoryExists( Path.Combine(destinationDir, "chef-settings-nested.yml")); File.WriteAllText(settingsFileNestedYaml, UtilsYaml.JsonToYaml(jsonSettingsArray)); } // Now replace the settings in the configuration templates foreach (var kvp in this.GetSettings().configuration_replacement_files ?? new Dictionary <string, string>()) { var sourcePath = UtilsSystem.CombinePaths(this.Deployment.appPath, kvp.Key); var destinationPath = UtilsSystem.CombinePaths(this.Deployment.appPath, kvp.Value); var contents = File.ReadAllText(sourcePath); if (destinationPath == sourcePath) { throw new Exception("Destination and source for configuration settings replacements cannot be the same."); } contents = replacer.DoReplace(contents); File.WriteAllText(destinationPath, contents); } }
protected string GetIniFilePath() { return(UtilsSystem.CombinePaths(this.Deployment.runtimePath, "php", "php.ini")); }
/// <inheritdoc cref="DeployerBase"/> public void deploy() { var settings = this.GetSettings(); this.Deployment.windowsUsername = "******" + this.Deployment.installedApplicationSettings.GetId(); if (this.Deployment.GetPreviousDeployment() != null && this.Deployment.GetPreviousDeployment().windowsUsername != this.Deployment.windowsUsername) { this.Logger.LogWarning( false, "Windows account username has changed from '{0}' to '{1}', removal of account and granted permissions must be performed manually.", this.Deployment.GetPreviousDeployment()?.windowsUsername, this.Deployment.windowsUsername); } UtilsWindowsAccounts.EnsureUserExists(this.Deployment.WindowsUsernameFqdn(), this.Deployment.GetWindowsPassword(), this.Deployment.installedApplicationSettings.GetId(), this.Logger, this.GlobalSettings.directoryPrincipal); // Legacy behaviour, if no userGroups defined, create a chef_users groups and add the users // to it if (!(this.GlobalSettings.userGroups ?? new List <string>()).Any()) { UtilsWindowsAccounts.EnsureGroupExists(LEGACY_CHEF_USERS_GROUPNAME, this.GlobalSettings.directoryPrincipal); UtilsWindowsAccounts.EnsureUserInGroup(this.Deployment.WindowsUsernameFqdn(), LEGACY_CHEF_USERS_GROUPNAME, this.Logger, this.GlobalSettings.directoryPrincipal); } // Add the user to the user groups foreach (var groupIdentifier in this.GlobalSettings.userGroups ?? new List <string>()) { UtilsWindowsAccounts.EnsureUserInGroup(this.Deployment.WindowsUsernameFqdn(), groupIdentifier, this.Logger, this.GlobalSettings.directoryPrincipal); } // Add the user to any user groups defined at the application level foreach (var groupIdentifier in settings.user_groups ?? new List <string>()) { UtilsWindowsAccounts.EnsureUserInGroup(this.Deployment.WindowsUsernameFqdn(), groupIdentifier, this.Logger, this.GlobalSettings.directoryPrincipal); } // Add any privileges if requested foreach (var privilegeName in settings.privileges ?? new List <string>()) { UtilsWindowsAccounts.SetRight(this.Deployment.WindowsUsernameFqdn(), privilegeName, this.Logger); } // Getting security right at the OS level here is a little bit picky... // in order to have REALPATH to work in PHP we need to be able to read all directories // in a path i.e. D:\webs\chef\appnumber1\ // What we will do is disconnect the USERS account here... string basePath = UtilsSystem.CombinePaths(this.GlobalSettings.GetDefaultApplicationStorage().path, this.Deployment.getShortId()); UtilsSystem.EnsureDirectoryExists(basePath, true); UtilsWindowsAccounts.DisablePermissionInheritance(basePath); UtilsWindowsAccounts.RemoveAccessRulesForIdentity(new SecurityIdentifier(UtilsWindowsAccounts.WELL_KNOWN_SID_USERS), basePath, this.Logger); UtilsWindowsAccounts.AddPermissionToDirectoryIfMissing(this.Deployment.WindowsUsernameFqdn(), basePath, FileSystemRights.ReadAndExecute, this.GlobalSettings.directoryPrincipal); // Store this in the application storage location. this.Deployment.runtimePath = UtilsSystem.CombinePaths(basePath, "runtime"); UtilsSystem.EnsureDirectoryExists(this.Deployment.runtimePath, true); this.Deployment.runtimePathWritable = UtilsSystem.CombinePaths(basePath, "runtime_writable"); UtilsSystem.EnsureDirectoryExists(this.Deployment.runtimePathWritable, true); // Due to compatibility reasons with environments such as PHP (that do not play well with network file URIs such as shared folders) // by default these two directories are symlinked to a local path if they are network paths. // Temp dir string localTempPath = UtilsSystem.CombinePaths(this.Deployment.runtimePath, "temp"); string remoteTempPath = UtilsSystem.CombinePaths(this.GlobalSettings.GetDefaultTempStorage().path, this.Deployment.installedApplicationSettings.GetId()); UtilsSystem.EnsureDirectoryExists(remoteTempPath, true); UtilsJunction.EnsureLink(localTempPath, remoteTempPath, this.Logger, false); this.Deployment.tempPath = localTempPath; // Temp dir sys this.Deployment.tempPathSys = UtilsSystem.CombinePaths(this.Deployment.runtimePathWritable, "_tmp"); UtilsSystem.EnsureDirectoryExists(this.Deployment.tempPathSys, true); // Log dir string localLogPath = UtilsSystem.CombinePaths(this.Deployment.runtimePath, "log"); string remoteLogPath = UtilsSystem.CombinePaths(this.GlobalSettings.GetDefaultLogStorage().path, this.Deployment.installedApplicationSettings.GetId()); UtilsSystem.EnsureDirectoryExists(remoteLogPath, true); UtilsJunction.EnsureLink(localLogPath, remoteLogPath, this.Logger, false); this.Deployment.logPath = localLogPath; this.Deployment.SetSetting("appstorage.base", basePath); this.Deployment.SetSetting("appstorage.temp", this.Deployment.tempPath); this.Deployment.SetSetting("appstorage.log", this.Deployment.logPath); this.Deployment.SetSetting("appstorage.remote_temp", remoteTempPath); this.Deployment.SetSetting("appstorage.remote_log", remoteLogPath); // We use this flag to detect transient storage // that must be removed when the deployer is "undeployed". AppBaseStorageType appBaseStorageType = AppBaseStorageType.Original; // TODO: Make this configurable through the chef.yml settings file. string ignoreOnDeployPattern = "^\\.git\\\\|^chef\\\\|^\\.vs\\\\"; switch (this.Deployment.installedApplicationSettings.GetApplicationMountStrategy()) { case ApplicationMountStrategy.Copy: this.Deployment.appPath = UtilsSystem.CombinePaths(basePath, "app"); // TODO: We should consider the ability to symlink the code here, or to point/mount directly // to the original source path. This would probably require delegating this step to the artifact downloader // (artifact.getDownloader()) or having the downloader tell us how to deal with this (symlinks, direct, whatever) this.Logger.LogInfo(true, "Copying artifact files..."); UtilsSystem.CopyFilesRecursivelyFast(this.Deployment.artifact.localPath, this.Deployment.appPath, false, ignoreOnDeployPattern, this.Logger); this.Logger.LogInfo(true, "Ensure app has proper user permissions for account '{0}'", this.Deployment.WindowsUsernameFqdn()); UtilsWindowsAccounts.AddPermissionToDirectoryIfMissing(this.Deployment.WindowsUserPrincipalName(), this.Deployment.appPath, FileSystemRights.ReadAndExecute, this.GlobalSettings.directoryPrincipal); this.Deployment.artifact.DeleteIfRemote(this.Logger); appBaseStorageType = AppBaseStorageType.Transient; break; case ApplicationMountStrategy.Move: this.Deployment.appPath = UtilsSystem.CombinePaths(basePath, "app"); // TODO: We should consider the ability to symlink the code here, or to point/mount directly // to the original source path. This would probably require delegating this step to the artifact downloader // (artifact.getDownloader()) or having the downloader tell us how to deal with this (symlinks, direct, whatever) this.Logger.LogInfo(true, "Moving artifact files..."); UtilsSystem.MoveDirectory(this.Deployment.artifact.localPath, this.Deployment.appPath, this.Logger, ignoreOnDeployPattern); // We had issues in appveyor where _webs location is in C drive and thus not giving // permissions here would make tests fail. this.Logger.LogInfo(true, "Ensure app has proper user permissions for account '{0}'", this.Deployment.WindowsUsernameFqdn()); UtilsWindowsAccounts.AddPermissionToDirectoryIfMissing(this.Deployment.WindowsUsernameFqdn(), this.Deployment.appPath, FileSystemRights.ReadAndExecute, this.GlobalSettings.directoryPrincipal); this.Deployment.artifact.DeleteIfRemote(this.Logger); appBaseStorageType = AppBaseStorageType.Transient; break; case ApplicationMountStrategy.Link: this.Logger.LogInfo(true, "Linking artifact files..."); this.Deployment.appPath = UtilsSystem.CombinePaths(basePath, "app"); UtilsJunction.EnsureLink(this.Deployment.appPath, this.Deployment.artifact.localPath, this.Logger, false); this.Logger.LogInfo(true, "Ensure app has proper user permissions for account '{0}'", this.Deployment.WindowsUsernameFqdn()); UtilsWindowsAccounts.AddPermissionToDirectoryIfMissing(this.Deployment.WindowsUsernameFqdn(), this.Deployment.artifact.localPath, FileSystemRights.ReadAndExecute, this.GlobalSettings.directoryPrincipal); appBaseStorageType = AppBaseStorageType.Symlink; break; case ApplicationMountStrategy.Original: this.Logger.LogInfo(true, "Ensure app has proper user permissions for account '{0}'", this.Deployment.WindowsUsernameFqdn()); UtilsWindowsAccounts.AddPermissionToDirectoryIfMissing(this.Deployment.WindowsUsernameFqdn(), this.Deployment.artifact.localPath, FileSystemRights.ReadAndExecute, this.GlobalSettings.directoryPrincipal); this.Deployment.appPath = UtilsSystem.CombinePaths(this.Deployment.artifact.localPath); appBaseStorageType = AppBaseStorageType.Original; break; default: throw new NotImplementedException("The requested mount strategy for the application is not available: " + this.Deployment.installedApplicationSettings.GetApplicationMountStrategy()); } this.Deployment.SetRuntimeSetting("deployment.appPath", this.Deployment.appPath); this.Deployment.SetRuntimeSetting("deployment.logPath", this.Deployment.logPath); this.Deployment.SetRuntimeSetting("deployment.tempPath", this.Deployment.tempPath); this.Deployment.SetSetting("appstorage.appBaseStorageType", appBaseStorageType); UtilsWindowsAccounts.AddPermissionToDirectoryIfMissing(this.Deployment.WindowsUsernameFqdn(), remoteTempPath, FileSystemRights.Write | FileSystemRights.Read | FileSystemRights.Delete, this.GlobalSettings.directoryPrincipal); UtilsWindowsAccounts.AddPermissionToDirectoryIfMissing(this.Deployment.WindowsUsernameFqdn(), remoteLogPath, FileSystemRights.Write | FileSystemRights.Read | FileSystemRights.Delete, this.GlobalSettings.directoryPrincipal); UtilsWindowsAccounts.AddPermissionToDirectoryIfMissing(this.Deployment.WindowsUsernameFqdn(), this.Deployment.runtimePath, FileSystemRights.ReadAndExecute, this.GlobalSettings.directoryPrincipal); UtilsWindowsAccounts.AddPermissionToDirectoryIfMissing(this.Deployment.WindowsUsernameFqdn(), this.Deployment.runtimePathWritable, FileSystemRights.Write | FileSystemRights.Read | FileSystemRights.Delete, this.GlobalSettings.directoryPrincipal); this.DeployFonts(settings); }
public void undeploy(bool isUninstall = false) { if (this.Deployment == null) { return; } var strategy = this.Deployment.installedApplicationSettings.GetApplicationMountStrategy(); string basePath = UtilsSystem.CombinePaths(this.GlobalSettings.GetDefaultApplicationStorage().path, this.Deployment.getShortId()); switch (strategy) { case ApplicationMountStrategy.Copy: case ApplicationMountStrategy.Move: UtilsSystem.DeleteDirectoryAndCloseProcesses(basePath, this.Logger, UtilsSystem.DefaultProcessWhitelist, 100); break; case ApplicationMountStrategy.Link: UtilsJunction.RemoveJunction(this.Deployment.appPath); UtilsSystem.DeleteDirectoryAndCloseProcesses(basePath, this.Logger, UtilsSystem.DefaultProcessWhitelist, 100); break; case ApplicationMountStrategy.Original: // Do nothing! break; default: throw new Exception("Option not supported."); } if (!isUninstall) { return; } var canonicalPath = this.Deployment.GetSetting <string>("appstorage.canonical", null, this.Logger); if (Directory.Exists(canonicalPath) && UtilsJunction.IsJunctionOrSymlink(canonicalPath)) { Directory.Delete(canonicalPath); } // Usually the IIS site has been closed a few fractions of a second // before this is called, so the folders have probably not yet // been released, waitPauseMs at least 10 seconds. UtilsSystem.DeleteDirectoryAndCloseProcesses(this.Deployment.GetSetting <string>("appstorage.temp", null, this.Logger), this.Logger, UtilsSystem.DefaultProcessWhitelist, 60); UtilsSystem.DeleteDirectoryAndCloseProcesses(this.Deployment.GetSetting <string>("appstorage.log", null, this.Logger), this.Logger, UtilsSystem.DefaultProcessWhitelist, 60); UtilsSystem.DeleteDirectoryAndCloseProcesses(this.Deployment.GetSetting <string>("appstorage.remote_temp", null, this.Logger), this.Logger, UtilsSystem.DefaultProcessWhitelist, 60); UtilsSystem.DeleteDirectoryAndCloseProcesses(this.Deployment.GetSetting <string>("appstorage.remote_log", null, this.Logger), this.Logger, UtilsSystem.DefaultProcessWhitelist, 60); var settings = this.GetSettings(); var groups = this.GlobalSettings.userGroups ?? new List <string>(); // add legacy group chef_users groups.Add(LEGACY_CHEF_USERS_GROUPNAME); // Remove user from all groups before deleting foreach (var groupIdentifier in groups) { UtilsWindowsAccounts.EnsureUserNotInGroup(this.Deployment.WindowsUsernameFqdn(), groupIdentifier, this.Logger, this.GlobalSettings.directoryPrincipal); } // Add the user to any user groups defined at the application level foreach (var groupIdentifier in settings.user_groups ?? new List <string>()) { UtilsWindowsAccounts.EnsureUserNotInGroup(this.Deployment.WindowsUsernameFqdn(), groupIdentifier, this.Logger, this.GlobalSettings.directoryPrincipal); } UtilsWindowsAccounts.DeleteUser(this.Deployment.WindowsUsernameFqdn(), this.GlobalSettings.directoryPrincipal); }
/// <summary> /// Provisions a certificate in the central store /// </summary> /// <param name="hostName">Domain to register</param> /// <param name="email">Registration e-mail</param> /// <param name="bindingInfo">IIS binding info</param> /// <param name="ownerSiteName">The site that owns the binding, used to assign identity and application pool permissions.</param> /// <param name="forceSelfSigned">Force a self-signed certificate</param> /// <param name="forceRenewal">Force renewal, even if renewal conditions are not met</param> /// <returns>The certificate's friendly name, ready to be bound in IIS</returns> public void ProvisionCertificateInIis( string hostName, string email, string bindingInfo, string ownerSiteName, bool forceSelfSigned = false, bool forceRenewal = false) { if (hostName.Contains("*")) { throw new Exception($"Provisioning certificates for wildcard host name '{hostName}' is not supported."); } var currentCertificate = UtilsIis.FindCertificateInCentralCertificateStore(hostName, this.Logger, out _); double remainingCertificateDays = (currentCertificate?.NotAfter - DateTime.Now)?.TotalDays ?? 0; this.Logger.LogInfo(true, "Total days remaining for certificate expiration: '{0}'", (int)Math.Floor(remainingCertificateDays)); // Trigger renovation. Do this differently on mock/prod environment. // Next renewal attempt is calculated based on previous renewal attempt var renewalState = this.GetCertificateRenewalState(hostName); // Legacy apps don't have this set, or when a certificate has been manually placed if (renewalState.NextRenewal == null && remainingCertificateDays > 1) { renewalState.NextRenewal = this.CalculateNextRenewalAttempt(hostName, (int)remainingCertificateDays); this.StoreCertificateRenewalState(renewalState); } int remainingDaysForNextRenewal = renewalState.NextRenewal == null ? 0 : (int)(renewalState.NextRenewal - DateTime.UtcNow).Value.TotalDays; int certificateTotalDuration = currentCertificate == null ? 0 : (int)(currentCertificate.NotAfter - currentCertificate.NotBefore).TotalDays; this.Logger.LogInfo(true, "Next renewal attempt for this site SSL targeted in '{0}' days.", remainingDaysForNextRenewal); // Check that the validationfailed request rate is not exceeded for this domain if (!forceRenewal && renewalState.FailedValidations.AsIterable().Count(i => (DateTime.UtcNow - i).TotalHours < 48) >= 2) { // Make this message verbos so that it will not flood the logs, the failed validation message will get logged // anyways and is sufficient. this.Logger.LogWarning(true, "The hostname '{0}' has reached the limit of two failed validations in the last 48 hours.", hostName); return; } if (!forceRenewal && !forceSelfSigned && remainingDaysForNextRenewal > 0 && remainingCertificateDays > 0) { this.Logger.LogWarning(true, "Next renewal attempt date not reached, skipping SSL provisioning."); return; } if (!forceRenewal && remainingDaysForNextRenewal > 0 && (remainingDaysForNextRenewal > certificateTotalDuration * 0.5) && certificateTotalDuration > 0) { this.Logger.LogWarning(false, "Certificate has not yet been through at least 50% of it's lifetime so it will not be renewed.'"); renewalState.NextRenewal = this.CalculateNextRenewalAttempt(hostName, (int)remainingCertificateDays); this.StoreCertificateRenewalState(renewalState); return; } // Check the general too many requests rate exceeded if (!forceRenewal && this.SimpleStoreRenewalStatus.Get <bool>("ssl-certificate-provider-too-many-requests", out var tooManyRequests)) { this.Logger.LogWarning(false, "Certificate provisioning temporarily disabled due to a Too Many Requests ACME error. Flag stored in {0}", tooManyRequests.StorePath); return; } this.Logger.LogInfo(false, "Attempting SSL certificate renewal for site '{0}' and host '{1}'", ownerSiteName, hostName); // Clear old validation failed requests if (renewalState.FailedValidations?.Any() == true) { // Only keep failed validations that happen in the last 5 days renewalState.FailedValidations = renewalState.FailedValidations .Where((i) => (DateTime.UtcNow - i).TotalDays < 5).ToList(); } // This is a little bit inconvenient but... the most reliable and compatible // way to do this is to setup a custom IIS website that uses the binding during // provisioning. long tempSiteId; List <Site> haltedSites = new List <Site>(); var tempSiteName = "cert-" + this.AppId; var tempSiteAppId = "cert-" + this.AppId; string tempHostName = "localcert-" + hostName; this.Logger.LogInfo(true, "Preparing temp site: " + tempSiteName); List <Site> conflictingSites; // Prepare the site using (ServerManager sm = new ServerManager()) { // Query the sites in a resilient way... conflictingSites = UtilsSystem.QueryEnumerable( sm.Sites, (s) => s.State == ObjectState.Started && s.Bindings.Any((i) => i.Host.Equals(hostName)), (s) => s, (s) => s.Name, this.Logger); } // Stop the sites that might prevent this one from starting foreach (var s in conflictingSites) { this.Logger.LogInfo(true, "Stopping site {0} to avoid binding collision.", s.Name); this.AppPoolUtils.WebsiteAction(s.Name, AppPoolActionType.Stop, skipApplicationPools: true); haltedSites.Add(s); } using (ServerManager sm = new ServerManager()) { // Make sure there is no other site (might be stuck?) var existingSite = (from p in sm.Sites where p.Name == tempSiteName select p).FirstOrDefault(); var tempSite = existingSite ?? sm.Sites.Add(tempSiteName, this.GetAcmeTemporarySiteRootForApplication(), 80); // Propagate application pool usage so that permissions are properly handled. var ownerSite = sm.Sites.First((i) => i.Name == ownerSiteName); tempSite.Applications.First().ApplicationPoolName = ownerSite.Applications.First().ApplicationPoolName; // Delete all bindings tempSite.Bindings.Clear(); tempSite.Bindings.Add(bindingInfo, "http"); tempSite.Bindings.Add($"{UtilsIis.LOCALHOST_ADDRESS}:80:" + tempHostName, "http"); tempSiteId = tempSite.Id; this.UtilsHosts.AddHostsMapping(UtilsIis.LOCALHOST_ADDRESS, tempHostName, tempSiteAppId); // Prepare the website contents var sourceDir = UtilsSystem.FindResourcePhysicalPath(typeof(IISDeployer), ".well-known"); UtilsSystem.CopyFilesRecursively(new DirectoryInfo(sourceDir), new DirectoryInfo(this.GetWellKnownSharedPathForApplication()), true); UtilsIis.CommitChanges(sm); } UtilsIis.WaitForSiteToBeAvailable(tempSiteName, this.Logger); UtilsIis.ConfigureAnonymousAuthForIisApplication(tempSiteName, this.Deployment.WindowsUsernameFqdn(), this.Deployment.GetWindowsPassword()); IAcmeSharpProvider provider = null; try { this.AppPoolUtils.WebsiteAction(tempSiteName, AppPoolActionType.Start); // Check that the site does work using the local binding var testDataUrl = $"http://{tempHostName}/.well-known/acme-challenge/test.html"; this.Logger.LogInfo(true, "Validating local challenge setup at: {0}", testDataUrl); if (!string.Equals(UtilsSystem.DownloadUriAsText(testDataUrl), "test data")) { throw new Exception($"Could not locally validate acme challenge site setup at {testDataUrl}"); } // Ssl registration configuration only depends on the e-mail and is signed as such string sslSignerAndRegistrationStoragePath = UtilsSystem.EnsureDirectoryExists( UtilsSystem.CombinePaths(this.StoragePath, "_ssl_config", StringFormating.Instance.ExtremeClean(email)), true); // Initialize the provider bool useMockProvider = this.MockEnvironment || forceSelfSigned; provider = useMockProvider ? (IAcmeSharpProvider) new AcmeSharpProviderMock(this.Logger, tempHostName) : this.GetAcmeProvider(this.Logger, hostName); var signerPath = Path.Combine(this.StoragePath, "_signer.xml"); var registrationPath = Path.Combine(sslSignerAndRegistrationStoragePath, "registration.json"); provider.InitRegistration(signerPath, registrationPath, email); string challengeUrl; string challengeContent; string challengeFilePath; try { provider.GenerateHttpChallenge( out challengeUrl, out challengeContent, out challengeFilePath); } catch (AcmeClient.AcmeWebException acmeException) { if (acmeException.Message.Contains("429")) { int waitHours = 1; this.SimpleStoreRenewalStatus.Set("ssl-certificate-provider-too-many-requests", true, 60 * waitHours); this.Logger.LogError("Let's encrypt too many requests issue. Certificate provisioning disabled for the next {0} hours.", waitHours); this.Logger.LogException(acmeException, EventLogEntryType.Warning); return; } throw; } // Write the challanege contents string challengeFullPath = Path.Combine(this.GetAcmeTemporarySiteRootForApplication(), challengeFilePath); File.WriteAllText( challengeFullPath, challengeContent); this.Logger.LogInfo(false, $"Veryfing challenge at '{challengeUrl}'"); try { // Validate that we can actually access the challenge ourselves! string contents = UtilsSystem.DownloadUriAsText(challengeUrl, false); if (!string.Equals(contents, challengeContent)) { throw new Exception( $"Could not validate ACME challenge, retrieved challenge '{contents}' does not match '{challengeContent}'"); } } catch (Exception e) { this.Logger.LogWarning(true, "Cannot self-verify auth challenge, this can sometimes happeen under some DNS setups. {0}", e.Message + Environment.NewLine + e.InnerException?.Message); } var challengeValidated = false; try { challengeValidated = provider.ValidateChallenge(); } catch (Exception e) { this.Logger.LogException(e, EventLogEntryType.Warning); } this.Logger.LogWarning(true, "Remote challenge validation success: " + (challengeValidated ? "Yes" : "No")); // Download the certificates to this temp location string temporaryCertificatePath = UtilsSystem.EnsureDirectoryExists(UtilsSystem.CombinePaths(this.StoragePath, this.AppId, "ssl_certificates", hostName), true); CertificatePaths certificatepaths = null; // This is here for testing purposes if (Environment.GetEnvironmentVariable("TEST_FAIL_CHALLENGE_VALIDATION") == true.ToString()) { challengeValidated = false; } if (!challengeValidated) { // There is a Failed Validation limit of 5 failures per account, per hostname, per hour. renewalState.FailedValidations = renewalState.FailedValidations ?? new List <DateTime>(); renewalState.FailedValidations.Add(DateTime.UtcNow); this.Logger.LogError( "Challenge could not be validated at '{0}'. If behind a load balancer, make sure that the site is deployed in ALL nodes, remove the self-signed certificate from the store and redeploy the application.", challengeUrl); this.StoreCertificateRenewalState(renewalState); } else { try { certificatepaths = provider.DownloadCertificate( UtilsEncryption.GetMD5(hostName), hostName, temporaryCertificatePath); } catch (AcmeClient.AcmeWebException acmeException) { this.Logger.LogException(acmeException, EventLogEntryType.Warning); } catch (WebException webException) { this.Logger.LogException(webException, EventLogEntryType.Warning); } catch (Exception e) { this.Logger.LogException(e, EventLogEntryType.Warning); } } if (certificatepaths == null && currentCertificate == null) { this.Logger.LogWarning(false, "Unable to acquire certificate and site does not have a valid existing one, using self-signed fallback."); provider = new AcmeSharpProviderMock(this.Logger, hostName); certificatepaths = provider.DownloadCertificate( UtilsEncryption.GetMD5(hostName), hostName, temporaryCertificatePath); } // Save this, use a fixed name certificate file if (certificatepaths != null) { string certificateFilePath = Path.Combine(UtilsIis.CentralStorePath(this.Logger), hostName + ".pfx"); UtilsSystem.RetryWhile( () => { File.Copy(certificatepaths.pfxPemFile, certificateFilePath, true); }, (e) => true, 2500, this.Logger); this.Logger.LogInfo(false, "Certificate file writen to '{0}'", certificateFilePath); // TODO: Activate this refreshing when it's prooved to work // UtilsIis.EnsureCertificateInCentralCertificateStoreIsRebound(hostName, this.Logger); } // Remove temporary certificates UtilsSystem.DeleteDirectory(temporaryCertificatePath, this.Logger); // Remove the already used challenge if it was validated. Otherwise keep it // for debugging purposes. if (challengeValidated && File.Exists(challengeFullPath)) { File.Delete(challengeFullPath); } // In the end, we always have a certificate. Program a renewal date according to the remaining expiration. currentCertificate = UtilsIis.FindCertificateInCentralCertificateStore(hostName, this.Logger, out _); remainingCertificateDays = (currentCertificate?.NotAfter - DateTime.Now)?.TotalDays ?? 0; // Add some randomness in renewal dates to avoid all certificates being renewed at once and reaching api limits renewalState.LastRenewal = DateTime.UtcNow; renewalState.NextRenewal = this.CalculateNextRenewalAttempt(hostName, (int)remainingCertificateDays); this.StoreCertificateRenewalState(renewalState); } finally { this.Logger.LogInfo(true, "Disposing temporary verification setup"); provider?.Dispose(); this.UtilsHosts.RemoveHostsMapping(tempSiteAppId); // Restore the original state of IIS!!! using (ServerManager sm = new ServerManager()) { var site = sm.Sites.Single(i => i.Id == tempSiteId); UtilsIis.RemoveSite(site, sm, this.Logger); UtilsIis.CommitChanges(sm); } // Give IIS some time to reconfigure itself and free resources. Thread.Sleep(1000); // Start the sites foreach (var site in haltedSites) { // Add some retry logic here because bringing the original sites online is critical UtilsSystem.RetryWhile(() => { this.AppPoolUtils.WebsiteAction(site.Name, AppPoolActionType.Start); }, (e) => true, 5000, this.Logger); } } }
protected string GetPhpExe() { return(UtilsSystem.CombinePaths(this.Deployment.runtimePath, "php", "php.exe")); }
public void deploy() { var diskSettings = this.DeployerSettings.castTo <DiskServiceSettings>(); var baseStoragePath = this.GetStoragePath(diskSettings); if (diskSettings.mounts == null || !diskSettings.mounts.Any()) { throw new Exception("You must specify at least a mount for a disk service."); } // Each one of these is to be mounted as a symlink/junction foreach (var mount in diskSettings.mounts) { if (string.IsNullOrWhiteSpace(diskSettings.id)) { throw new Exception("Disk settings must have an id"); } if (string.IsNullOrWhiteSpace(mount.Value.id)) { throw new Exception("All mounts in disk configuration must have an id"); } // Expand the local path.. var mountDestination = UtilsSystem.EnsureDirectoryExists(UtilsSystem.CombinePaths(baseStoragePath, mount.Value.path), true); this.Logger.LogInfo(true, "Mounting disk '{0}' at {1}", mount.Value.id, mountDestination); var settingkey = $"services.{diskSettings.id}.mount.{mount.Value.id}.path"; // We might sometimes need to force a specific path in an environment... if (this.Deployment.installedApplicationSettings.GetRuntimeSettingsOverrides().ContainsKey(settingkey)) { string newMountDestination = this.Deployment.installedApplicationSettings.GetRuntimeSettingsOverrides()[settingkey]; if (Directory.Exists(newMountDestination)) { this.Logger.LogInfo(false, "Default mount for '{0}' overriden with '{1}' from a default value of '{2}'.", settingkey, newMountDestination, mountDestination); mountDestination = newMountDestination; } else { this.Logger.LogInfo(false, "Tried to override mount path ({0}) with a non-existent directory: '{1}'", settingkey, newMountDestination); } } // Ensure proper permissions this.Logger.LogInfo(true, "Ensure mount has proper user permissions for account '{0}'", this.Deployment.WindowsUsernameFqdn()); UtilsWindowsAccounts.AddPermissionToDirectoryIfMissing(this.Deployment.WindowsUsernameFqdn(), mountDestination, FileSystemRights.Modify, this.GlobalSettings.directoryPrincipal); string mountPath = null; if (!string.IsNullOrWhiteSpace(mount.Value.mountpath)) { mountPath = UtilsSystem.CombinePaths(this.Deployment.appPath, mount.Value.mountpath); UtilsJunction.EnsureLink(mountPath, mountDestination, this.Logger, mount.Value.persist_on_deploy); } // Wether we requested or not a mountpath, make a link in the runtime folder to all disk stores var localMountPath = UtilsSystem.CombinePaths(this.Deployment.runtimePath, "disk", mount.Value.id); this.Logger.LogInfo(true, "Linking disk at local path {0}", localMountPath); UtilsSystem.EnsureDirectoryExists(UtilsSystem.CombinePaths(this.Deployment.runtimePath, "disk"), true); UtilsJunction.EnsureLink(localMountPath, mountDestination, this.Logger, mount.Value.persist_on_deploy); // Make only the local mount path visible to the application this.Deployment.SetRuntimeSetting(settingkey, localMountPath); this.Deployment.SetSettingCollection($"service.{diskSettings.id}", settingkey, new DiskStore() { path = localMountPath, junction = mountPath, originalPath = mountDestination, junctionRealPath = UtilsJunction.ResolvePath(mountPath) }); } }
/// <summary> /// Archive and cleanup the log directories... /// /// TODO: This is hardcoded here... should be a true cron /// that each deployer implements the way they want... /// </summary> protected void CleanupLogDirectories() { var logPath = this.GlobalSettings.GetDefaultLogStorage().path; this.Logger.LogInfo(true, "Cleaning log directories at path: {0}", logPath); var files = Directory.EnumerateFiles(logPath, "*", SearchOption.AllDirectories); foreach (string f in files) { try { FileInfo info = new FileInfo(f); // Delete any files not touched within the last six months. if ((DateTime.Now - info.LastWriteTime).TotalDays > (30 * 6) && (info.FullName.EndsWith("_bak.zip") || info.Extension.ToLower() == ".log" || info.Extension.ToLower() == ".txt")) { File.Delete(f); this.Logger.LogInfo(true, "Deleted log file: {0}", info.FullName); continue; } // Zip any log files that are larger than 100Mb or // have not been writen into in the last 30 days. bool extensionCriteria = (info.Extension.ToLower() == ".log" || info.Extension.ToLower() == ".txt"); bool timeCriteria = (DateTime.UtcNow - info.LastWriteTimeUtc).TotalDays > 30; bool sizeCriteria = info.Length > 1024 * 1024 * 100; if (extensionCriteria && (timeCriteria && sizeCriteria)) { string name = info.FullName; string extensionlessName = name.Replace(info.Extension, string.Empty); string folderTemp = extensionlessName; Directory.CreateDirectory(extensionlessName); info.MoveTo(UtilsSystem.CombinePaths(extensionlessName, info.Name)); string diff = info.CreationTime.ToString("yyyyMMddHHmmss"); ZipFile.CreateFromDirectory(folderTemp, extensionlessName + "_" + diff + "_bak.zip"); Directory.Delete(folderTemp, true); this.Logger.LogInfo(true, "Archived log file: {0}", info.FullName); } } catch (UnauthorizedAccessException e) { this.Logger.LogException(e, EventLogEntryType.Warning); } catch (FileNotFoundException e) { this.Logger.LogException(e, EventLogEntryType.Warning); } catch (Exception e) { this.Logger.LogException(e, EventLogEntryType.Warning); } } }
/// <summary> /// Execute the opreation... /// </summary> /// <param name="destination"></param> /// <param name="forceDownload"></param> protected void DoExecute( string destination, bool forceDownload = false) { var uri = this.Config.uri; var maps = this.Config.maps; var filename = Path.GetFileName(uri); var tmpDir = UtilsSystem.GetTempPath("iischef_cache", UtilsEncryption.GetMD5(uri)); var tmpFile = UtilsSystem.CombinePaths(UtilsSystem.GetTempPath(), UtilsEncryption.GetMD5(uri) + "_" + filename); if (forceDownload && Directory.Exists(tmpDir)) { Directory.Delete(tmpDir, true); } if (Directory.Exists(tmpDir)) { var difo = new DirectoryInfo(tmpDir); if (!difo.EnumerateFiles("*", SearchOption.AllDirectories).Any()) { Directory.Delete(tmpDir, true); } } if (!Directory.Exists(tmpDir)) { var parsedUri = new Uri(uri); if (parsedUri.Scheme.Equals("file", StringComparison.CurrentCultureIgnoreCase)) { var path = Path.Combine(this.LocalArtifactPath, parsedUri.LocalPath.TrimStart("\\".ToCharArray())); File.Copy(path, tmpFile); } else { using (var wc = new WebClient()) { try { wc.Headers.Add( "User-Agent", "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/46.0.2490.33 Safari/537.36"); wc.DownloadFile(uri, tmpFile); } catch (Exception ex) { throw new Exception("Could not download file: " + uri, ex); } } } UtilsSystem.EnsureDirectoryExists(tmpDir, true); if (tmpFile.EndsWith(".zip")) { ZipFile.ExtractToDirectory(tmpFile, tmpDir); } else { File.Move(tmpFile, UtilsSystem.CombinePaths(tmpDir, filename)); } File.Delete(tmpFile); } // Move the files according to the maps foreach (var map in maps) { var files = (new DirectoryInfo(tmpDir)).GetFiles(map.Key, SearchOption.AllDirectories); if (!files.Any()) { throw new Exception( string.Format( "No matching files found for pattern: {0} in package {1} ['{2}']", map.Key, uri, tmpDir)); } if (files.Count() == 1) { var dest = UtilsSystem.CombinePaths(destination, map.Value); UtilsSystem.EnsureDirectoryExists(dest); File.Copy(files.First().FullName, dest); } else { foreach (var f in files) { var subpath = f.FullName.Replace((new DirectoryInfo(tmpDir)).FullName, string.Empty); var dest = UtilsSystem.CombinePaths(destination, map.Value, subpath); UtilsSystem.EnsureDirectoryExists(dest); try { File.Copy(f.FullName, dest); } catch (Exception e) { throw new Exception($"Error copying file '{f.FullName}' to '{dest}'"); } } } } }
protected void DeployPhpRuntimeShortcut() { string command = $"{this.GetPhpExe()} -c \"{this.GetIniFilePath()}\" %*"; var destionationDir = UtilsSystem.EnsureDirectoryExists(UtilsSystem.CombinePaths(this.Deployment.runtimePath, "include_path"), true); File.WriteAllText(UtilsSystem.CombinePaths(destionationDir, "php.bat"), command); File.WriteAllText( UtilsSystem.CombinePaths(destionationDir, "setenv.bat"), string.Format( @" set path={0};%path% cd /D ""{1}"" ", destionationDir.Replace("\"", "\"\""), this.Deployment.appPath.Replace("\"", "\"\""))); File.WriteAllText( UtilsSystem.CombinePaths(destionationDir, "setenv.ps1"), string.Format( @" $Env:Path=""{0};$($Env:Path)""; CD ""{1}"" ", destionationDir.Replace("\"", "\"\""), this.Deployment.appPath.Replace("\"", "\"\""))); File.WriteAllText( UtilsSystem.CombinePaths(destionationDir, "launch_console.bat"), "cmd /k setenv.bat"); File.WriteAllText(UtilsSystem.CombinePaths(destionationDir, "launch_console_admin_UAC.bat"), @" @echo off set _SCRIPT_DRIVE=%~d0 set _SCRIPT_PATH=%~p0 call :isAdmin if %errorlevel% == 0 ( goto :run ) else ( echo Requesting administrative privileges... goto :UACPrompt ) exit /b :isAdmin fsutil dirty query %systemdrive% >nul exit /b :run REM <YOUR BATCH CODE GOES HERE> %_SCRIPT_DRIVE% cd %_SCRIPT_PATH% cmd /k setenv.bat exit /b :UACPrompt echo Set UAC = CreateObject^(""Shell.Application""^) > ""%temp%\getadmin.vbs"" echo UAC.ShellExecute ""cmd.exe"", ""/c %~s0 %~1"", """", ""runas"", 1 >> ""%temp%\getadmin.vbs"" ""%temp%\getadmin.vbs"" del ""%temp%\getadmin.vbs"" exit / B` "); }
/// <summary> /// Downloads (And extracts) single artifacts from jobs. /// </summary> /// <param name="applicationId"></param> /// <param name="build"></param> /// <param name="artifactRegex"></param> /// <param name="destinationPath"></param> /// <param name="logger"></param> public void DownloadSingleArtifactFromBuild( string applicationId, Build build, string artifactRegex, string destinationPath, ILoggerInterface logger) { UtilsSystem.EnsureDirectoryExists(destinationPath, true); // Use the first job in the build... var job = build.jobs.First(); var artifact = this.FindDefaultArtifactForBuild(job, build, artifactRegex); var filename = artifact.fileName; var extension = Path.GetExtension(filename); string downloadTemporaryDir = UtilsSystem.EnsureDirectoryExists(UtilsSystem.CombinePaths(this.TempDir, "_appveyor", "dld", applicationId), true); int artifactRetentionNum = 5; int artifactAgeHoursForStale = 24; // Do not touch the latest artifactRetentionNum artifacts or artifacts that are not older than artifactAgeHoursForStale hours var staleFiles = Directory.EnumerateFiles(downloadTemporaryDir) .Select((i) => new FileInfo(i)) .Where((i) => i.Extension.Equals(".zip", StringComparison.CurrentCultureIgnoreCase)) .OrderByDescending((i) => i.CreationTimeUtc) .Skip(artifactRetentionNum) .Where((i) => (DateTime.UtcNow - i.LastWriteTime).TotalHours > artifactAgeHoursForStale) .ToList(); foreach (var f in staleFiles) { // Make this fail proof, it's just a cleanup. try { this.Logger.LogInfo(true, "Removing stale artifact cache file {0}", f.FullName); f.Delete(); } catch { // ignored } } // Use a short hash as the temporary file name, because long paths can have issues... var tmpFile = UtilsSystem.CombinePaths(downloadTemporaryDir, UtilsEncryption.GetShortHash(JsonConvert.SerializeObject(build) + filename) + extension); if (Path.GetExtension(tmpFile)?.ToLower() != ".zip") { throw new NotImplementedException("AppVeyor artifacts should only be Zip Files."); } if (!File.Exists(tmpFile)) { // Use an intermediate .tmp file just in case the files does not finish to download, // if it exists, clear it. string tmpFileDownload = tmpFile + ".tmp"; if (File.Exists(tmpFileDownload)) { UtilsSystem.RetryWhile(() => File.Delete(tmpFileDownload), (e) => true, 4000, this.Logger); } var url = $"/api/buildjobs/{job.jobId}/artifacts/{filename}"; logger.LogInfo(true, "Downloading artifact from: '{0}' to '{1}'", url, tmpFileDownload); this.ExecuteApiCallToFile(url, tmpFileDownload); // Rename to the final cached artifact file logger.LogInfo(true, "Download succesful, moving to '{0}'", tmpFile); UtilsSystem.RetryWhile(() => File.Move(tmpFileDownload, tmpFile), (e) => true, 4000, this.Logger); } else { logger.LogInfo(true, "Skipping artifact download, already in local cache: {0}", tmpFile); } logger.LogInfo(true, "Unzipping {1} file to '{0}'...", destinationPath, UtilsSystem.BytesToString(new FileInfo(tmpFile).Length)); ZipFile.ExtractToDirectory(tmpFile, destinationPath); logger.LogInfo(true, "Unzipping finished."); }