Заметки

Реклама:

Оглавление

27.07.2017
Servers - Архивирование в AWS Glacier (часть 5)

В этой заметке рассказывается о базе данных для регистрации архивов, вспомогательных командных файлах и расписании запусков скриптов.

Скрипт для создания таблицы, в которую записывается информация об архивах:

USE [AWS-Glacier]
GO

/****** Object:  Table [dbo].[agc-prod]    Script Date: 7/27/2017 9:49:56 AM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[archive-2017](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [archive-name] [nvarchar](255) NULL,
    [aws-account-id] [nvarchar](255) NULL,
    [aws-vault-name] [nvarchar](255) NULL,
    [aws-archive-id] [nvarchar](255) NULL,
    [archive-content] [ntext] NULL,
    [creation-timestamp] [datetime] NULL,
    [last-access-timestamp] [datetime] NULL,
    [state] [nvarchar](255) NULL,
    [notice] [nvarchar](255) NULL,
 CONSTRAINT [PK_Glacier-Content] PRIMARY KEY CLUSTERED 
(
    [id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]

GO

Скрипт запуска скрипта архивирования и копирования в облако - AWS-1-BackupAndTransfer.cmd:

powershell.exe "%~DP0Process-AWSGlacierBackup-Prod.ps1" 
powershell.exe "%~DP0Process-AWSGlacierBackup-Serv.ps1"

Скрипт запуска скрипта инвентаризации - AWS-2-Inventory.cmd:

powershell.exe "%~DP0Process-AWSGlacierInventory-Prod.ps1"
powershell.exe "%~DP0Process-AWSGlacierInventory-Serv.ps1"

Скрипт запуска скрипта сверки архивов на сервере и в облаке - AWS-3-Checking.cmd:

powershell.exe "%~DP0Process-AWSGlacierChecking-Prod.ps1"
powershell.exe "%~DP0Process-AWSGlacierChecking-Serv.ps1"

Все три вышеописанных скрипта необходимо поставить в планировщик запуска задач на сервере. Я рекомендую запускать скрипты по такому расписанию:

  • Пятница после рабочего дня - AWS-1-BackupAndTransfer.cmd
  • Воскресенье в середине дня - AWS-2-Inventory.cmd
  • Понедельник перед рабочим днем - AWS-3-Checking.cmd

Все задания нужно запускать с повышенными привилегиями (RunLevel = HighestAvailable) и от имени системы NT AUTHORITY\SYSTEM (UserId = S-1-5-18).

 

25.07.2017
Servers - Архивирование в AWS Glacier (часть 4)

В этой заметке описывается скрипт для проверки содержимого облачного архива AWS Glacier и удаления локальных файлов, успешно размещенных в облаке.

Логика скрипта:

  • Найти файл с номером задания.
  • Подключиться к AWS Glacier и скачать результат инвентаризации коллекции архивов.
  • В базе данных компании найти все архивы с состоянием transfered.
  • Сравнить размеры локального архива и файла в облаке.
  • Удалить локальный архив и пометить его состояние в базе данных как archived.

Код скрипта:

# Folder for saving archives
$ArchiveFolder = "C:\2_Transfer"

# Notifications
$MailServer = "mail.domain.com"
$MailEncoding = [System.Text.Encoding]::UTF8
$MailFrom = "$env:computername@domain.com"
$MailSubject = "Glacier backup"
[string[]]$MailTO = "admin@domain.com"
[string[]]$MailCC = ""

# AWS variables
$AWSAccountID = 'AWS_Account_ID'
$AWSRegion = 'AWS_region'
$AWSVaultName = 'AWS_Vault_Name'
$AWSProfileAccessKey = "AWS_Access_Key"
$AWSProfileSecretKey = "AWS_Secret_Key"
$AWSJobIdFileName = "AWSGlacier-InventoryJobId.txt"

# SQL variables
$SQLServer = 'SQLServer.domain.com'
$SQLDatabase = 'AWS-Glacier'
$SQLTable = '[archive-2017]'
$SQLUsername = 'db_user'
$SQLPassword = 'db_password'

# Registering AWS libraries
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\Net45\AWSSDK.Core.dll"
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\Net45\AWSSDK.Glacier.dll"

Function Write-ScriptLog {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Message,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$LogFile
  )
  Process {
    $LogMessage = Get-Date -uformat "%d.%m.%Y %H:%M:%S"
    $LogMessage += "`t"
    $LogMessage += $Message
    $LogMessage | Out-File -FilePath $LogFile -Append
  }
}#End Function

Function Invoke-Query {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$ServerInstance,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Database,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Username,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Password,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Query
  )
  Process {
    $ConnectionString = "Server=$ServerInstance;uid=$Username;pwd=$Password;Database=$Database;Integrated Security=False;"
    $Connection = New-Object System.Data.SqlClient.SqlConnection
    $Connection.ConnectionString = $ConnectionString
    $Connection.Open()
    $Command = New-Object System.Data.SQLClient.SQLCommand
    $Command.Connection = $Connection
    $Command.CommandText = $Query
    $Result = $Command.ExecuteReader()
    $Datatable = New-Object “System.Data.DataTable”
    $Datatable.Load($Result)
    $Connection.Close()
    Return $Datatable
  }
}#End Function

Function Invoke-NonQuery {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$ServerInstance,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Database,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Username,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Password,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Query
  )
  Process {
    $ConnectionString = "Server=$ServerInstance;uid=$Username;pwd=$Password;Database=$Database;Integrated Security=False;"
    $Connection = New-Object System.Data.SqlClient.SqlConnection
    $Connection.ConnectionString = $ConnectionString
    $Connection.Open()
    $Command = New-Object System.Data.SQLClient.SQLCommand
    $Command.Connection = $Connection
    $Command.CommandText = $Query
    $Result = $Command.ExecuteNonQuery()
    $Connection.Close()
  }
}#End Function

# --- Start ---

# Calculating variables
$CurrentDate = Get-Date
$ScriptFolder = $MyInvocation.MyCommand.Path.SubString(0,($MyInvocation.MyCommand.Path.Length `
  - $MyInvocation.MyCommand.Name.Length))
$LogFile = $ScriptFolder + 'Logs\' + (Get-Date -format yyyy_MM_dd) + "_" `
  + $MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + ".log"
  
# Log-file creation
If (-not(Test-Path ($ScriptFolder + 'Logs') -PathType Container )) {
  New-Item -ItemType Directory -Path ($ScriptFolder + 'Logs')
}
Out-File -FilePath $LogFile

Write-ScriptLog -LogFile $LogFile -Message ($MyInvocation.MyCommand.Name + " started")

Write-ScriptLog -LogFile $LogFile -Message ("============================== Input data ==============================")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Account ID = $AWSAccountID")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Region = $AWSRegion")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Vault Name = $AWSVaultName")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Job Id File Name = $AWSJobIdFileName")

Write-ScriptLog -LogFile $LogFile -Message ("============================== Processing ==============================")

# Input file name
$AWSJobIdFilePath = $ScriptFolder + $AWSJobIdFileName
$ProcessedArchives = $Nothing
$NotProcessedArchives = $Nothing

If (Test-Path($AWSJobIdFilePath)) {

  Write-ScriptLog -LogFile $LogFile -Message ("Reading $AWSJobOutputFileName file")
  $AWSGlacerJobId = Get-Content $AWSJobIdFilePath
  Write-ScriptLog -LogFile $LogFile -Message ("Inventory retrieval job id is $AWSGlacerJobId")

  Write-ScriptLog -LogFile $LogFile -Message ("Connecting to AWS")
  $AWSEndpoint = [Amazon.RegionEndpoint]::GetBySystemName($AWSRegion)
  # Set inventory job for a AWS Glacier vault
  $AWSGlacierClient = [Amazon.Glacier.AmazonGlacierClient]::New($AWSProfileAccessKey, $AWSProfileSecretKey, $AWSEndpoint)
  Write-ScriptLog -LogFile $LogFile -Message ("Connection to AWS Glacier is opened")

  Write-ScriptLog -LogFile $LogFile -Message ("Requesting job output")
  $AWSGlacierJobOutputRequest  = [Amazon.Glacier.Model.GetJobOutputRequest]::new()
  $AWSGlacierJobOutputRequest.AccountId = $AWSAccountID
  $AWSGlacierJobOutputRequest.VaultName = $AWSVaultName
  $AWSGlacierJobOutputRequest.JobId = $AWSGlacerJobId
  Try {
    $AWSGlacierOutputResult = $AWSGlacierClient.GetJobOutput($AWSGlacierJobOutputRequest)
    Write-ScriptLog -LogFile $LogFile -Message ("Job output is received with status " + $AWSGlacierOutputResult.Status)
  }
  Catch {
    Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
  }

  If ($AWSGlacierOutputResult -ne $Nothing) {
      Write-ScriptLog -LogFile $LogFile -Message ("Parsing job output")
      [Byte[]]$buffer = New-Object System.Byte[] 4096
      $EncodedText = New-Object -TypeName System.Text.ASCIIEncoding
      $AWSGlacierOutputResultContent = $Nothing
      While(($i = $AWSGlacierOutputResult.Body.Read($buffer, 0, $buffer.Length)) -ne 0) {
        $AWSGlacierOutputResultContent += $EncodedText.GetString($buffer, 0, $i)
      }
      $AWSGlacierContent = $AWSGlacierOutputResultContent | ConvertFrom-Json
      Write-ScriptLog -LogFile $LogFile -Message ("Job output is parsed")
      Write-ScriptLog -LogFile $LogFile -Message ("There are " + $AWSGlacierContent.ArchiveList.Count + " archives in the vault")
      $AWSGlacierClient.Dispose()
      Write-ScriptLog -LogFile $LogFile -Message ("Connection to AWS Glacier is closed")

      Write-ScriptLog -LogFile $LogFile -Message ("Updating archives statuses in the database")
      # Requesting archives from the database
      $Query = "SELECT * FROM " + $SQLTable + " WHERE [state] = 'transfered'"
      $TransferedFiles = Invoke-Query -ServerInstance $SQLServer -Database $SQLDatabase -Username $SQLUsername `
        -Password $SQLPassword -Query $Query
      Write-ScriptLog -LogFile $LogFile -Message ("There are " + $TransferedFiles.Count + " archives transfered to AWS Glacier")
      ForEach ($CurrentFile in $TransferedFiles) {
        $CurrentFileId = $CurrentFile."id"
        $CurrentFileName = $CurrentFile."archive-name"
        $CurrentFileAwsId = $CurrentFile."aws-archive-id"
        $CurrentFilePath = $ArchiveFolder + "\" + $CurrentFileName
        # Looking for current archive id in the vault
        Write-ScriptLog -LogFile $LogFile -Message ("  $CurrentFileName is waiting for successfull transrefing confirmation")
        ForEach ($AWSArchive in $AWSGlacierContent.ArchiveList) {
          If ($AWSArchive.ArchiveId -eq $CurrentFileAwsId) {
            Write-ScriptLog -LogFile $LogFile -Message ("    $CurrentFileName is found is the vault")
            If ((Get-Item $CurrentFilePath).Length -eq $AWSArchive.Size) {
              Write-ScriptLog -LogFile $LogFile -Message ("    The size of the archives at local server and at AWS are the same")
              Try {
                Remove-Item $CurrentFilePath -Force -Confirm:$False -ErrorAction Stop
                Write-ScriptLog -LogFile $LogFile -Message ("    $CurrentFilePath is deleted")
              } Catch {
                Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
              }
              Write-ScriptLog -LogFile $LogFile -Message ("    Updating the arhive status in the database")
              $SQLAWSArchiveId = $AWSTransferResult.ArchiveId
              $SQLAccessTimeStamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
              $SQLState = "archived"
              $SQLNotice = "size: " + $AWSArchive.size + " bytes, creation: " + $AWSArchive.CreationDate
              $UpdateQuery = "UPDATE " + $SQLTable + " SET [state] = '$SQLState',"
              $UpdateQuery += " [notice] = '$SQLNotice', [last-access-timestamp] = '$SQLAccessTimeStamp' WHERE [id] = $CurrentFileId"
              Invoke-NonQuery -ServerInstance $SQLServer -Database $SQLDatabase -Username $SQLUsername `
                -Password $SQLPassword -Query $UpdateQuery
              $ProcessedArchives += $CurrentFileName + "; "
            } Else {
              $FailedArchives += $CurrentFileName + "; "
            }
          }
        }
      }
      Write-ScriptLog -LogFile $LogFile -Message ("Archives statuses are updated the database")
     
      Send-MailMessage -SmtpServer $MailServer -Encoding $MailEncoding -From $MailFrom -To $MailTo -CC $MailCC `
        -Subject $MailSubject -BodyAsHtml "Successfully processed archives: $ProcessedArchives<br>" + `
          "Failed archives: $FailedArchives<br />Please review $LogFile on $env:computername for details."

      Write-ScriptLog -LogFile $LogFile -Message ("Deleting $AWSJobIdFileName file")
      Try {
        Remove-Item $AWSJobIdFilePath -Force -Confirm:$False -ErrorAction Stop
      Write-ScriptLog -LogFile $LogFile -Message ("$AWSJobIdFileName file is deleted")
      } Catch {
        Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
      }

  } Else {
    Write-ScriptLog -LogFile $LogFile -Message ("Job output is empty")

    Send-MailMessage -SmtpServer $MailServer -Encoding $MailEncoding -From $MailFrom -To $MailTo -CC $MailCC `
      -Subject $MailSubject -BodyAsHtml "AWS job output is empty<br />Please review $LogFile on $env:computername for details."

    If (((Get-Date) - (get-item $AWSJobIdFilePath).CreationTime).Days -ge 2) {
      Write-ScriptLog -LogFile $LogFile -Message ("$AWSJobIdFileName file is outdated")
      Write-ScriptLog -LogFile $LogFile -Message ("Deleting $AWSJobIdFileName file")
      Try {
        Remove-Item $AWSJobIdFilePath -Force -Confirm:$False -ErrorAction Stop
      Write-ScriptLog -LogFile $LogFile -Message ("$AWSJobIdFileName file is deleted")
      } Catch {
        Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
      }
    }
  }
}
Else {
  Write-ScriptLog -LogFile $LogFile -Message ("The processing is stopped")
  Write-ScriptLog -LogFile $LogFile -Message ("Use Process-AWSGlacierInventory.ps1 script to get information about AWS archives")
}

# --- End ---
Write-ScriptLog -LogFile $LogFile -Message ("========================================================================")
Write-ScriptLog -LogFile $LogFile -Message ($MyInvocation.MyCommand.Name + " stopped")

Входные данные скрипта:

  • ArchiveFolder - путь, куда скрипт сохраняет ZIP-архивы
  • MailServer - почтовый сервер компании для отправки уведомлений о работе скрипта
  • MailEncoding - кодировка писем
  • MailFrom - от чьего имени отправляются письма
  • MailSubject - тема писем
  • MailTO - кому отправлять письма
  • MailCC - кому отправлять копии писем
  • AWSAccountID - ID учетной записи Amazon Web Services
  • AWSRegion - регион, где находится коллекция архивов
  • AWSVaultName - имя коллекции архивов
  • AWSProfileAccessKey - учетная запись AWS с правом доступа к коллекции архивов
  • AWSProfileSecretKey - ключ к учетной записи AWS
  • SQLServer - SQL сервер компании
  • SQLDatabase - имя базы данных
  • SQLTable - имя таблицы
  • SQLUsername - имя пользователя с правом вносить изменения в SQL базу данных
  • SQLPassword - пароль пользователя SQL
  • AWSJobIdFileName - имя файла, в котором нужно сохранить номер задания (файл сохраняется в той же папке, где лежит скрипт)

Результат работы скрипта:

  • Завершенная процедура архивирования файла в облаке AWS Glacier.
  • Лог-файл работы скрипта.
Продолжение в заметке "Servers - Архивирование в AWS Glacier (часть 5)".
 

24.07.2017
Servers - Архивирование в AWS Glacier (часть 3)

В этой заметке описывается скрипт для установки задания по инвентаризации облачного архива AWS Glacier для того, чтобы удостовериться, что файл, скопированный на предыдущем этапе, успешно размещен в облаке.

Логика скрипта:

  • Подключиться к AWS Glacier.
  • Создать задание на инвентаризацию коллекции архивов.
  • Создать файл с номером задания, чтобы через 4-12 часов считать результаты инвентаризации, основываясь на записанном в файле номере задания.

Код скрипта:

# AWS variables
$AWSAccountID = 'AWS_Account_ID'
$AWSRegion = 'AWS_region'
$AWSVaultName = 'AWS_Vault_Name'
$AWSProfileAccessKey = "AWS_Access_Key"
$AWSProfileSecretKey = "AWS_Secret_Key"
$AWSJobIdFileName = "AWSGlacier-InventoryJobId.txt"

# Registering AWS libraries
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\Net45\AWSSDK.Core.dll"
Add-Type -Path "C:\Program Files (x86)\AWS SDK for .NET\bin\Net45\AWSSDK.Glacier.dll"

Function Write-ScriptLog {
  Param(
    [CmdletBinding()] 
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$Message,
    [Parameter(ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
    [String]$LogFile
  )
  Process {
    $LogMessage = Get-Date -uformat "%d.%m.%Y %H:%M:%S"
    $LogMessage += "`t"
    $LogMessage += $Message
    $LogMessage | Out-File -FilePath $LogFile -Append
  }
}#End Function

# --- Start ---

# Calculating variables
$CurrentDate = Get-Date
$ScriptFolder = $MyInvocation.MyCommand.Path.SubString(0,($MyInvocation.MyCommand.Path.Length - `
  $MyInvocation.MyCommand.Name.Length))
$LogFile = $ScriptFolder + 'Logs\' + (Get-Date -format yyyy_MM_dd) + "_" + `
  $MyInvocation.MyCommand.Name.SubString(0,($MyInvocation.MyCommand.Name.Length - 4)) + ".log"

# Log-file creation
If (-not(Test-Path ($ScriptFolder + 'Logs') -PathType Container )) {
  New-Item -ItemType Directory -Path ($ScriptFolder + 'Logs')
}
Out-File -FilePath $LogFile

Write-ScriptLog -LogFile $LogFile -Message ($MyInvocation.MyCommand.Name + " started")

Write-ScriptLog -LogFile $LogFile -Message ("============================== Input data ==============================")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Account ID = $AWSAccountID")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Region = $AWSRegion")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Vault Name = $AWSVaultName")
Write-ScriptLog -LogFile $LogFile -Message ("AWS Job Id File Name = $AWSJobIdFileName")

Write-ScriptLog -LogFile $LogFile -Message ("============================== Processing ==============================")

# Output file name
$AWSJobIdFilePath = $ScriptFolder + $AWSJobIdFileName

If (-not (Test-Path($AWSJobIdFilePath))) {
  $AWSEndpoint = [Amazon.RegionEndpoint]::GetBySystemName($AWSRegion)
  # Set inventory job for a AWS Glacier vault
  $AWSGlacierClient = [Amazon.Glacier.AmazonGlacierClient]::New($AWSProfileAccessKey, $AWSProfileSecretKey, $AWSEndpoint)
  Write-ScriptLog -LogFile $LogFile -Message ("Connection to AWS Glacier is opened")
  $AWSGlacierJobParameters  = [Amazon.Glacier.Model.JobParameters]::new()
  $AWSGlacierJobParameters.Type = "inventory-retrieval"
  $AWSGlacierJobParameters.Description = "Retrieve inventory of a vault"
  $AWSGlacierJobRequest  = [Amazon.Glacier.Model.InitiateJobRequest]::new($AWSAccountID, $AWSVaultName, $AWSGlacierJobParameters)
  Try {
    $AWSGlacerJobId = $AWSGlacierClient.InitiateJob($AWSGlacierJobRequest).JobId
    Write-ScriptLog -LogFile $LogFile -Message ("Inventory retrieval job is set with Id $AWSGlacerJobId")
  }
  Catch {
    Write-ScriptLog -LogFile $LogFile -Message ("----> Error: " + $_.Exception.Message)
  }
  # Saving the result to the output file
  If ($AWSGlacerJobId -ne $Nothing) {
    $AWSGlacerJobId | Out-File -FilePath $AWSJobIdFilePath
    $AWSGlacierClient.Dispose()
  } Else {
    Write-ScriptLog -LogFile $LogFile -Message ("Job is not set")
  }
  Write-ScriptLog -LogFile $LogFile -Message ("Connection to AWS Glacier is closed")
}
Else {
  Write-ScriptLog -LogFile $LogFile -Message ("The processing is stopped")
  Write-ScriptLog -LogFile $LogFile -Message ("Use Process-AWSGlacierChecking.ps1 script to process existing Job Id")
}

# --- End ---
Write-ScriptLog -LogFile $LogFile -Message ("========================================================================")
Write-ScriptLog -LogFile $LogFile -Message ($MyInvocation.MyCommand.Name + " stopped")

Входные данные скрипта:

  • AWSAccountID - ID учетной записи Amazon Web Services
  • AWSRegion - регион, где находится коллекция архивов
  • AWSVaultName - имя коллекции архивов
  • AWSProfileAccessKey - учетная запись AWS с правом доступа к коллекции архивов
  • AWSProfileSecretKey - ключ к учетной записи AWS
  • AWSJobIdFileName - имя файла, в котором нужно сохранить номер задания (файл сохраняется в той же папке, где лежит скрипт)

Результат работы скрипта:

  • Установленное задание на инвентаризацию коллекции архивов в AWS Glacier.
  • Файл с номером задания по инвентаризации.
  • Лог-файл работы скрипта.
Продолжение в заметке "Servers - Архивирование в AWS Glacier (часть 4)".
 

 

Вверх