bosh-director 1.5.0.pre.1113

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. data/CHANGELOG +34 -0
  2. data/bin/bosh-director +36 -0
  3. data/bin/bosh-director-console +84 -0
  4. data/bin/bosh-director-drain-workers +42 -0
  5. data/bin/bosh-director-migrate +58 -0
  6. data/bin/bosh-director-scheduler +27 -0
  7. data/bin/bosh-director-worker +76 -0
  8. data/db/migrations/README +1 -0
  9. data/db/migrations/director/20110209010747_initial.rb +118 -0
  10. data/db/migrations/director/20110406055800_add_task_user.rb +9 -0
  11. data/db/migrations/director/20110518225809_remove_cid_constrain.rb +13 -0
  12. data/db/migrations/director/20110617211923_add_deployments_release_versions.rb +32 -0
  13. data/db/migrations/director/20110622212607_add_task_checkpoint_timestamp.rb +9 -0
  14. data/db/migrations/director/20110628023039_add_state_to_instances.rb +21 -0
  15. data/db/migrations/director/20110709012332_add_disk_size_to_instances.rb +9 -0
  16. data/db/migrations/director/20110906183441_add_log_bundles.rb +11 -0
  17. data/db/migrations/director/20110907194830_add_logs_json_to_templates.rb +9 -0
  18. data/db/migrations/director/20110915205610_add_persistent_disks.rb +51 -0
  19. data/db/migrations/director/20111005180929_add_properties.rb +14 -0
  20. data/db/migrations/director/20111110024617_add_deployment_problems.rb +24 -0
  21. data/db/migrations/director/20111216214145_recreate_support_for_vms.rb +9 -0
  22. data/db/migrations/director/20120102084027_add_credentials_to_vms.rb +7 -0
  23. data/db/migrations/director/20120427235217_allow_multiple_releases_per_deployment.rb +36 -0
  24. data/db/migrations/director/20120524175805_add_task_type.rb +44 -0
  25. data/db/migrations/director/20120614001930_delete_redundant_deployment_release_relation.rb +34 -0
  26. data/db/migrations/director/20120822004528_add_fingerprint_to_templates_and_packages.rb +17 -0
  27. data/db/migrations/director/20120830191244_add_properties_to_templates.rb +9 -0
  28. data/db/migrations/director/20121106190739_persist_vm_env.rb +9 -0
  29. data/db/migrations/director/20130222232131_add_sha1_to_stemcells.rb +9 -0
  30. data/db/migrations/director/20130312211407_add_commit_hash_to_release_versions.rb +19 -0
  31. data/db/migrations/director/20130409235338_snapshot.rb +15 -0
  32. data/db/migrations/director/20130530164918_add_paused_flag_to_instance.rb +14 -0
  33. data/db/migrations/director/20130531172604_add_director_attributes.rb +13 -0
  34. data/db/migrations/dns/20120123234908_initial.rb +27 -0
  35. data/lib/bosh/director.rb +133 -0
  36. data/lib/bosh/director/agent_client.rb +78 -0
  37. data/lib/bosh/director/api.rb +29 -0
  38. data/lib/bosh/director/api/api_helper.rb +81 -0
  39. data/lib/bosh/director/api/backup_manager.rb +15 -0
  40. data/lib/bosh/director/api/controller.rb +639 -0
  41. data/lib/bosh/director/api/controller_helpers.rb +34 -0
  42. data/lib/bosh/director/api/deployment_lookup.rb +13 -0
  43. data/lib/bosh/director/api/deployment_manager.rb +60 -0
  44. data/lib/bosh/director/api/http_constants.rb +16 -0
  45. data/lib/bosh/director/api/instance_lookup.rb +44 -0
  46. data/lib/bosh/director/api/instance_manager.rb +63 -0
  47. data/lib/bosh/director/api/problem_manager.rb +40 -0
  48. data/lib/bosh/director/api/property_manager.rb +69 -0
  49. data/lib/bosh/director/api/release_manager.rb +59 -0
  50. data/lib/bosh/director/api/resource_manager.rb +69 -0
  51. data/lib/bosh/director/api/resurrector_manager.rb +15 -0
  52. data/lib/bosh/director/api/snapshot_manager.rb +94 -0
  53. data/lib/bosh/director/api/stemcell_manager.rb +50 -0
  54. data/lib/bosh/director/api/task_helper.rb +46 -0
  55. data/lib/bosh/director/api/task_manager.rb +64 -0
  56. data/lib/bosh/director/api/user_manager.rb +72 -0
  57. data/lib/bosh/director/api/vm_state_manager.rb +11 -0
  58. data/lib/bosh/director/app.rb +35 -0
  59. data/lib/bosh/director/blob_util.rb +87 -0
  60. data/lib/bosh/director/blobstores.rb +29 -0
  61. data/lib/bosh/director/client.rb +156 -0
  62. data/lib/bosh/director/cloudcheck_helper.rb +204 -0
  63. data/lib/bosh/director/compile_task.rb +157 -0
  64. data/lib/bosh/director/config.rb +370 -0
  65. data/lib/bosh/director/configuration_hasher.rb +114 -0
  66. data/lib/bosh/director/cycle_helper.rb +36 -0
  67. data/lib/bosh/director/db_backup.rb +22 -0
  68. data/lib/bosh/director/db_backup/adapter.rb +3 -0
  69. data/lib/bosh/director/db_backup/adapter/mysql2.rb +27 -0
  70. data/lib/bosh/director/db_backup/adapter/postgres.rb +36 -0
  71. data/lib/bosh/director/db_backup/adapter/sqlite.rb +17 -0
  72. data/lib/bosh/director/db_backup/error.rb +10 -0
  73. data/lib/bosh/director/deployment_plan.rb +26 -0
  74. data/lib/bosh/director/deployment_plan/assembler.rb +430 -0
  75. data/lib/bosh/director/deployment_plan/compilation_config.rb +54 -0
  76. data/lib/bosh/director/deployment_plan/compiled_package.rb +35 -0
  77. data/lib/bosh/director/deployment_plan/dynamic_network.rb +91 -0
  78. data/lib/bosh/director/deployment_plan/idle_vm.rb +109 -0
  79. data/lib/bosh/director/deployment_plan/instance.rb +413 -0
  80. data/lib/bosh/director/deployment_plan/job.rb +470 -0
  81. data/lib/bosh/director/deployment_plan/manual_network.rb +137 -0
  82. data/lib/bosh/director/deployment_plan/network.rb +74 -0
  83. data/lib/bosh/director/deployment_plan/network_subnet.rb +167 -0
  84. data/lib/bosh/director/deployment_plan/planner.rb +288 -0
  85. data/lib/bosh/director/deployment_plan/preparer.rb +52 -0
  86. data/lib/bosh/director/deployment_plan/release.rb +126 -0
  87. data/lib/bosh/director/deployment_plan/resource_pool.rb +143 -0
  88. data/lib/bosh/director/deployment_plan/resource_pools.rb +68 -0
  89. data/lib/bosh/director/deployment_plan/stemcell.rb +56 -0
  90. data/lib/bosh/director/deployment_plan/template.rb +94 -0
  91. data/lib/bosh/director/deployment_plan/update_config.rb +80 -0
  92. data/lib/bosh/director/deployment_plan/updater.rb +55 -0
  93. data/lib/bosh/director/deployment_plan/vip_network.rb +79 -0
  94. data/lib/bosh/director/dns_helper.rb +204 -0
  95. data/lib/bosh/director/download_helper.rb +44 -0
  96. data/lib/bosh/director/duration.rb +36 -0
  97. data/lib/bosh/director/encryption_helper.rb +10 -0
  98. data/lib/bosh/director/errors.rb +198 -0
  99. data/lib/bosh/director/event_log.rb +136 -0
  100. data/lib/bosh/director/ext.rb +64 -0
  101. data/lib/bosh/director/hash_string_vals.rb +13 -0
  102. data/lib/bosh/director/instance_deleter.rb +109 -0
  103. data/lib/bosh/director/instance_updater.rb +506 -0
  104. data/lib/bosh/director/ip_util.rb +67 -0
  105. data/lib/bosh/director/job_queue.rb +16 -0
  106. data/lib/bosh/director/job_runner.rb +162 -0
  107. data/lib/bosh/director/job_updater.rb +121 -0
  108. data/lib/bosh/director/jobs/backup.rb +86 -0
  109. data/lib/bosh/director/jobs/base_job.rb +66 -0
  110. data/lib/bosh/director/jobs/cloud_check/apply_resolutions.rb +46 -0
  111. data/lib/bosh/director/jobs/cloud_check/scan.rb +38 -0
  112. data/lib/bosh/director/jobs/cloud_check/scan_and_fix.rb +73 -0
  113. data/lib/bosh/director/jobs/create_snapshot.rb +23 -0
  114. data/lib/bosh/director/jobs/delete_deployment.rb +183 -0
  115. data/lib/bosh/director/jobs/delete_deployment_snapshots.rb +34 -0
  116. data/lib/bosh/director/jobs/delete_release.rb +219 -0
  117. data/lib/bosh/director/jobs/delete_snapshots.rb +23 -0
  118. data/lib/bosh/director/jobs/delete_stemcell.rb +102 -0
  119. data/lib/bosh/director/jobs/fetch_logs.rb +99 -0
  120. data/lib/bosh/director/jobs/scheduled_backup.rb +38 -0
  121. data/lib/bosh/director/jobs/snapshot_deployment.rb +61 -0
  122. data/lib/bosh/director/jobs/snapshot_deployments.rb +23 -0
  123. data/lib/bosh/director/jobs/snapshot_self.rb +43 -0
  124. data/lib/bosh/director/jobs/ssh.rb +59 -0
  125. data/lib/bosh/director/jobs/update_deployment.rb +110 -0
  126. data/lib/bosh/director/jobs/update_release.rb +672 -0
  127. data/lib/bosh/director/jobs/update_stemcell.rb +109 -0
  128. data/lib/bosh/director/jobs/vm_state.rb +89 -0
  129. data/lib/bosh/director/lock.rb +133 -0
  130. data/lib/bosh/director/lock_helper.rb +92 -0
  131. data/lib/bosh/director/models.rb +29 -0
  132. data/lib/bosh/director/models/compiled_package.rb +33 -0
  133. data/lib/bosh/director/models/deployment.rb +22 -0
  134. data/lib/bosh/director/models/deployment_problem.rb +49 -0
  135. data/lib/bosh/director/models/deployment_property.rb +21 -0
  136. data/lib/bosh/director/models/director_attribute.rb +9 -0
  137. data/lib/bosh/director/models/dns.rb +9 -0
  138. data/lib/bosh/director/models/dns/domain.rb +9 -0
  139. data/lib/bosh/director/models/dns/record.rb +7 -0
  140. data/lib/bosh/director/models/helpers/model_helper.rb +7 -0
  141. data/lib/bosh/director/models/instance.rb +28 -0
  142. data/lib/bosh/director/models/log_bundle.rb +10 -0
  143. data/lib/bosh/director/models/package.rb +30 -0
  144. data/lib/bosh/director/models/persistent_disk.rb +13 -0
  145. data/lib/bosh/director/models/release.rb +17 -0
  146. data/lib/bosh/director/models/release_version.rb +16 -0
  147. data/lib/bosh/director/models/snapshot.rb +13 -0
  148. data/lib/bosh/director/models/stemcell.rb +18 -0
  149. data/lib/bosh/director/models/task.rb +10 -0
  150. data/lib/bosh/director/models/template.rb +44 -0
  151. data/lib/bosh/director/models/user.rb +11 -0
  152. data/lib/bosh/director/models/vm.rb +42 -0
  153. data/lib/bosh/director/nats_rpc.rb +54 -0
  154. data/lib/bosh/director/network_reservation.rb +121 -0
  155. data/lib/bosh/director/next_rebase_version.rb +20 -0
  156. data/lib/bosh/director/package_compiler.rb +423 -0
  157. data/lib/bosh/director/problem_handlers/base.rb +153 -0
  158. data/lib/bosh/director/problem_handlers/inactive_disk.rb +112 -0
  159. data/lib/bosh/director/problem_handlers/invalid_problem.rb +28 -0
  160. data/lib/bosh/director/problem_handlers/missing_vm.rb +34 -0
  161. data/lib/bosh/director/problem_handlers/mount_info_mismatch.rb +62 -0
  162. data/lib/bosh/director/problem_handlers/out_of_sync_vm.rb +64 -0
  163. data/lib/bosh/director/problem_handlers/unbound_instance_vm.rb +85 -0
  164. data/lib/bosh/director/problem_handlers/unresponsive_agent.rb +78 -0
  165. data/lib/bosh/director/problem_resolver.rb +103 -0
  166. data/lib/bosh/director/problem_scanner.rb +268 -0
  167. data/lib/bosh/director/resource_pool_updater.rb +216 -0
  168. data/lib/bosh/director/scheduler.rb +57 -0
  169. data/lib/bosh/director/sequel.rb +13 -0
  170. data/lib/bosh/director/tar_gzipper.rb +47 -0
  171. data/lib/bosh/director/task_result_file.rb +19 -0
  172. data/lib/bosh/director/thread_pool.rb +8 -0
  173. data/lib/bosh/director/validation_helper.rb +55 -0
  174. data/lib/bosh/director/version.rb +7 -0
  175. data/lib/bosh/director/vm_creator.rb +80 -0
  176. data/lib/bosh/director/vm_data.rb +63 -0
  177. data/lib/bosh/director/vm_metadata_updater.rb +29 -0
  178. data/lib/bosh/director/vm_reuser.rb +63 -0
  179. data/lib/cloud/dummy.rb +149 -0
  180. metadata +664 -0
@@ -0,0 +1,61 @@
1
+ module Bosh::Director
2
+ module Jobs
3
+ class SnapshotDeployment < BaseJob
4
+ @queue = :normal
5
+
6
+ attr_reader :deployment
7
+
8
+ def self.job_type
9
+ :snapshot_deployment
10
+ end
11
+
12
+ def initialize(deployment_name, options = {})
13
+ @deployment = deployment_manager.find_by_name(deployment_name)
14
+ @options = options
15
+ @errors = 0
16
+ end
17
+
18
+ def deployment_manager
19
+ @deployment_manager ||= Bosh::Director::Api::DeploymentManager.new
20
+ end
21
+
22
+ def perform
23
+ logger.info("taking snapshot of: #{deployment.name}")
24
+ deployment.job_instances.each do |instance|
25
+ snapshot(instance)
26
+ end
27
+
28
+ msg = "snapshots of deployment '#{deployment.name}' created"
29
+ msg += ", with #{@errors} failure(s)" unless @errors == 0
30
+ msg
31
+ end
32
+
33
+ def snapshot(instance)
34
+ logger.info("taking snapshot of: #{instance.job}/#{instance.index} (#{instance.vm.cid})")
35
+ Bosh::Director::Api::SnapshotManager.take_snapshot(instance, @options)
36
+ rescue Bosh::Clouds::CloudError => e
37
+ @errors += 1
38
+ logger.error("failed to take snapshot of: #{instance.job}/#{instance.index} (#{instance.vm.cid}) - #{e.inspect}")
39
+ send_alert(instance, e.inspect)
40
+ end
41
+
42
+ ERROR = 3
43
+
44
+ def send_alert(instance, message)
45
+ nats = Bosh::Director::Config.nats
46
+ payload = Yajl::Encoder.encode(
47
+ {
48
+ "id" => 'director',
49
+ "severity" => ERROR,
50
+ "title" => "director - snapshot failure",
51
+ "summary" => "failed to snapshot #{instance.job}/#{instance.index}: #{message}",
52
+ "created_at" => Time.now.to_i
53
+ }
54
+ )
55
+
56
+ nats.publish('hm.director.alert', payload)
57
+ end
58
+ end
59
+ end
60
+ end
61
+
@@ -0,0 +1,23 @@
1
+ module Bosh::Director
2
+ module Jobs
3
+ class SnapshotDeployments < BaseJob
4
+ @queue = :normal
5
+
6
+ def self.job_type
7
+ :snapshot_deployments
8
+ end
9
+
10
+ def initialize(options={})
11
+ @snapshot_manager = options.fetch(:snapshot_manager) { Bosh::Director::Api::SnapshotManager.new }
12
+ end
13
+
14
+ def perform
15
+ tasks = Models::Deployment.all.map do |deployment|
16
+ @snapshot_manager.create_deployment_snapshot_task('scheduler', deployment)
17
+ end
18
+
19
+ "Enqueued snapshot tasks [#{tasks.map(&:id).join(', ')}]"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,43 @@
1
+ module Bosh::Director
2
+ module Jobs
3
+ class SnapshotSelf < BaseJob
4
+ @queue = :normal
5
+
6
+ def self.job_type
7
+ :snapshot_self
8
+ end
9
+
10
+ def initialize(options={})
11
+ @cloud = options.fetch(:cloud) { Config.cloud }
12
+ @director_uuid = options.fetch(:director_uuid) { Config.uuid }
13
+ @director_name = options.fetch(:director_name) { Config.name }
14
+ @enable_snapshots = options.fetch(:enable_snapshots) { Config.enable_snapshots }
15
+ end
16
+
17
+ def perform
18
+ unless @enable_snapshots
19
+ logger.info('Snapshots are disabled; skipping')
20
+ return
21
+ end
22
+
23
+ vm_id = @cloud.current_vm_id
24
+ disks = @cloud.get_disks(vm_id)
25
+ metadata = {
26
+ deployment: 'self',
27
+ job: 'director',
28
+ index: 0,
29
+ director_name: @director_name,
30
+ director_uuid: @director_uuid,
31
+ agent_id: 'self',
32
+ instance_id: vm_id
33
+ }
34
+
35
+ disks.each { |disk| @cloud.snapshot_disk(disk, metadata) }
36
+
37
+ "Snapshot director disks [#{disks.join(', ')}]"
38
+ rescue Bosh::Clouds::NotImplemented
39
+ logger.info('CPI does not support disk snapshots; skipping')
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,59 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Director
4
+ module Jobs
5
+ class Ssh < BaseJob
6
+ DEFAULT_SSH_DATA_LIFETIME = 300
7
+ SSH_TAG = "ssh"
8
+ @queue = :normal
9
+
10
+ def self.job_type
11
+ :ssh
12
+ end
13
+
14
+ def initialize(deployment_id, options = {})
15
+ @deployment_id = deployment_id
16
+ @target = options["target"]
17
+ @command = options["command"]
18
+ @params = options["params"]
19
+ @blobstore = options.fetch(:blobstore) { App.instance.blobstores.blobstore }
20
+ @instance_manager = Api::InstanceManager.new
21
+ end
22
+
23
+ def perform
24
+ job = @target["job"]
25
+ indexes = @target["indexes"]
26
+
27
+ filter = {
28
+ :deployment_id => @deployment_id
29
+ }
30
+
31
+ if indexes && indexes.size > 0
32
+ filter[:index] = indexes
33
+ end
34
+
35
+ if job
36
+ filter[:job] = job
37
+ end
38
+
39
+ instances = @instance_manager.filter_by(filter)
40
+
41
+ ssh_info = instances.map do |instance|
42
+ agent = @instance_manager.agent_client_for(instance)
43
+
44
+ logger.info("ssh #{@command} `#{instance.job}/#{instance.index}'")
45
+ result = agent.ssh(@command, @params)
46
+ result["index"] = instance.index
47
+
48
+ result
49
+ end
50
+
51
+ result_file.write(Yajl::Encoder.encode(ssh_info))
52
+ result_file.write("\n")
53
+
54
+ # task result
55
+ nil
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,110 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ module Bosh::Director
4
+ module Jobs
5
+ class UpdateDeployment < BaseJob
6
+ include LockHelper
7
+
8
+ @queue = :normal
9
+
10
+ def self.job_type
11
+ :update_deployment
12
+ end
13
+
14
+ # @param [String] manifest_file Path to deployment manifest
15
+ # @param [Hash] options Deployment options
16
+ def initialize(manifest_file, options = {})
17
+ logger.info('Reading deployment manifest')
18
+ @manifest_file = manifest_file
19
+ @manifest = File.read(@manifest_file)
20
+ logger.debug("Manifest:\n#{@manifest}")
21
+
22
+ logger.info('Creating deployment plan')
23
+ logger.info("Deployment plan options: #{options.pretty_inspect}")
24
+
25
+ plan_options = {
26
+ 'recreate' => !!options['recreate'],
27
+ 'job_states' => options['job_states'] || {},
28
+ 'job_rename' => options['job_rename'] || {}
29
+ }
30
+
31
+ manifest_as_hash = Psych.load(@manifest)
32
+ @deployment_plan = DeploymentPlan::Planner.new(manifest_as_hash, plan_options)
33
+ @deployment_plan.parse
34
+ logger.info('Created deployment plan')
35
+
36
+ resource_pools = @deployment_plan.resource_pools
37
+ @resource_pool_updaters = resource_pools.map do |resource_pool|
38
+ ResourcePoolUpdater.new(resource_pool)
39
+ end
40
+ end
41
+
42
+ def prepare
43
+ @assembler = DeploymentPlan::Assembler.new(@deployment_plan)
44
+ preparer = DeploymentPlan::Preparer.new(self, @assembler)
45
+
46
+ begin_stage('Preparing deployment', 9)
47
+ preparer.prepare
48
+
49
+ logger.info('Compiling and binding packages')
50
+ PackageCompiler.new(@deployment_plan).compile
51
+ end
52
+
53
+ def update
54
+ resource_pools = DeploymentPlan::ResourcePools.new(event_log, @resource_pool_updaters)
55
+
56
+ updater = DeploymentPlan::Updater.new(self, event_log, resource_pools, @assembler, @deployment_plan)
57
+ updater.update
58
+ end
59
+
60
+ def update_stemcell_references
61
+ current_stemcells = Set.new
62
+ @deployment_plan.resource_pools.each do |resource_pool|
63
+ current_stemcells << resource_pool.stemcell.model
64
+ end
65
+
66
+ deployment = @deployment_plan.model
67
+ stemcells = deployment.stemcells
68
+ stemcells.each do |stemcell|
69
+ unless current_stemcells.include?(stemcell)
70
+ stemcell.remove_deployment(deployment)
71
+ end
72
+ end
73
+ end
74
+
75
+ def perform
76
+ with_deployment_lock(@deployment_plan) do
77
+ logger.info('Preparing deployment')
78
+ prepare
79
+ begin
80
+ deployment = @deployment_plan.model
81
+ logger.info('Finished preparing deployment')
82
+ logger.info('Updating deployment')
83
+ update
84
+
85
+ with_release_locks(@deployment_plan) do
86
+ deployment.db.transaction do
87
+ deployment.remove_all_release_versions
88
+ # Now we know that deployment has succeeded and can remove
89
+ # previous partial deployments release version references
90
+ # to be able to delete these release versions later.
91
+ @deployment_plan.releases.each do |release|
92
+ deployment.add_release_version(release.model)
93
+ end
94
+ end
95
+ end
96
+
97
+ deployment.manifest = @manifest
98
+ deployment.save
99
+ logger.info('Finished updating deployment')
100
+ "/deployments/#{deployment.name}"
101
+ ensure
102
+ update_stemcell_references
103
+ end
104
+ end
105
+ ensure
106
+ FileUtils.rm_rf(@manifest_file)
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,672 @@
1
+ # Copyright (c) 2009-2012 VMware, Inc.
2
+
3
+ require 'common/version_number'
4
+
5
+ module Bosh::Director
6
+ module Jobs
7
+ class UpdateRelease < BaseJob
8
+ include LockHelper
9
+ include DownloadHelper
10
+
11
+ @queue = :normal
12
+
13
+ attr_accessor :release_model
14
+ attr_accessor :tmp_release_dir
15
+
16
+ def self.job_type
17
+ :update_release
18
+ end
19
+
20
+ # @param [String] tmp_release_dir Directory containing release bundle
21
+ # @param [Hash] options Release update options
22
+ def initialize(tmp_release_dir, options = {})
23
+ @tmp_release_dir = tmp_release_dir
24
+ @release_model = nil
25
+ @release_version_model = nil
26
+
27
+ @rebase = !!options["rebase"]
28
+ @package_rebase_mapping = {}
29
+ @job_rebase_mapping = {}
30
+
31
+ @manifest = nil
32
+ @name = nil
33
+ @version = nil
34
+
35
+ @packages_unchanged = false
36
+ @jobs_unchanged = false
37
+
38
+ @remote_release = options['remote'] || false
39
+ @remote_release_location = options['location'] if @remote_release
40
+ end
41
+
42
+ # Extracts release tarball, verifies release manifest and saves release
43
+ # in DB
44
+ # @return [void]
45
+ def perform
46
+ logger.info("Processing update release")
47
+ if @rebase
48
+ logger.info("Release rebase will be performed")
49
+ end
50
+
51
+ single_step_stage("Downloading remote release") { download_remote_release } if @remote_release
52
+ single_step_stage("Extracting release") { extract_release }
53
+ single_step_stage("Verifying manifest") { verify_manifest }
54
+
55
+ with_release_lock(@name) { process_release }
56
+
57
+ if @rebase && @packages_unchanged && @jobs_unchanged
58
+ raise DirectorError,
59
+ "Rebase is attempted without any job or package changes"
60
+ end
61
+
62
+ "Created release `#{@name}/#{@version}'"
63
+ rescue Exception => e
64
+ remove_release_version_model
65
+ raise e
66
+ ensure
67
+ if @tmp_release_dir && File.exists?(@tmp_release_dir)
68
+ FileUtils.rm_rf(@tmp_release_dir)
69
+ end
70
+ end
71
+
72
+ def download_remote_release
73
+ release_file = File.join(@tmp_release_dir, Api::ReleaseManager::RELEASE_TGZ)
74
+ download_remote_file('release', @remote_release_location, release_file)
75
+ end
76
+
77
+ # Extracts release tarball
78
+ # @return [void]
79
+ def extract_release
80
+ release_tgz = File.join(@tmp_release_dir,
81
+ Api::ReleaseManager::RELEASE_TGZ)
82
+
83
+ result = Bosh::Exec.sh("tar -C #{@tmp_release_dir} -xzf #{release_tgz} 2>&1", :on_error => :return)
84
+ if result.failed?
85
+ logger.error("Extracting release archive failed in dir #{@tmp_release_dir}, " +
86
+ "tar returned #{result.exit_status}, " +
87
+ "output: #{result.output}")
88
+ raise ReleaseInvalidArchive, "Extracting release archive failed. Check task debug log for details."
89
+ end
90
+ ensure
91
+ if release_tgz && File.exists?(release_tgz)
92
+ FileUtils.rm(release_tgz)
93
+ end
94
+ end
95
+
96
+ # @return [void]
97
+ def verify_manifest
98
+ manifest_file = File.join(@tmp_release_dir, "release.MF")
99
+ unless File.file?(manifest_file)
100
+ raise ReleaseManifestNotFound, "Release manifest not found"
101
+ end
102
+
103
+ @manifest = Psych.load_file(manifest_file)
104
+ normalize_manifest
105
+
106
+ @name = @manifest["name"]
107
+ @version = @manifest["version"]
108
+ @commit_hash = @manifest.fetch("commit_hash", nil)
109
+ @uncommitted_changes = @manifest.fetch("uncommitted_changes", nil)
110
+ end
111
+
112
+ # Processes uploaded release, creates jobs and packages in DB if needed
113
+ # @return [void]
114
+ def process_release
115
+ @release_model = Models::Release.find_or_create(:name => @name)
116
+ if @rebase
117
+ @version = next_release_version
118
+ end
119
+
120
+ version_attrs = {
121
+ :release => @release_model,
122
+ :version => @version
123
+ }
124
+ version_attrs[:uncommitted_changes] = @uncommitted_changes if @uncommitted_changes
125
+ version_attrs[:commit_hash] = @commit_hash if @commit_hash
126
+
127
+ @release_version_model = Models::ReleaseVersion.new(version_attrs)
128
+ unless @release_version_model.valid?
129
+ raise ReleaseAlreadyExists,
130
+ "Release `#{@name}/#{@version}' already exists"
131
+ end
132
+
133
+ @release_version_model.save
134
+
135
+ single_step_stage("Resolving package dependencies") do
136
+ resolve_package_dependencies(@manifest["packages"])
137
+ end
138
+
139
+ @packages = {}
140
+ process_packages
141
+ process_jobs
142
+
143
+ unless @package_rebase_mapping.empty?
144
+ event_log.begin_stage(
145
+ "Rebased packages", @package_rebase_mapping.size)
146
+ @package_rebase_mapping.each_pair do |name, transition|
147
+ event_log.track("#{name}: #{transition}") {}
148
+ end
149
+ end
150
+
151
+ unless @job_rebase_mapping.empty?
152
+ event_log.begin_stage(
153
+ "Rebased jobs", @job_rebase_mapping.size)
154
+ @job_rebase_mapping.each_pair do |name, transition|
155
+ event_log.track("#{name}: #{transition}") {}
156
+ end
157
+ end
158
+
159
+ event_log.begin_stage("Release has been created", 1)
160
+ event_log.track("#{@name}/#{@version}") {}
161
+ end
162
+
163
+ # Normalizes release manifest, so all names, versions, and checksums
164
+ # are Strings.
165
+ # @return [void]
166
+ def normalize_manifest
167
+ Bosh::Director.hash_string_vals(@manifest, 'name', 'version')
168
+
169
+ @manifest['packages'].each { |p| Bosh::Director.hash_string_vals(p, 'name', 'version', 'sha1') }
170
+ @manifest['jobs'].each { |j| Bosh::Director.hash_string_vals(j, 'name', 'version', 'sha1') }
171
+ end
172
+
173
+ # Resolves package dependencies, makes sure there are no cycles
174
+ # and all dependencies are present
175
+ # @return [void]
176
+ def resolve_package_dependencies(packages)
177
+ packages_by_name = {}
178
+ packages.each do |package|
179
+ packages_by_name[package["name"]] = package
180
+ package["dependencies"] ||= []
181
+ end
182
+ dependency_lookup = lambda do |package_name|
183
+ packages_by_name[package_name]["dependencies"]
184
+ end
185
+ result = CycleHelper.check_for_cycle(packages_by_name.keys,
186
+ :connected_vertices => true,
187
+ &dependency_lookup)
188
+
189
+ packages.each do |package|
190
+ name = package["name"]
191
+ dependencies = package["dependencies"]
192
+
193
+ logger.info("Resolving package dependencies for `#{name}', " +
194
+ "found: #{dependencies.pretty_inspect}")
195
+ package["dependencies"] = result[:connected_vertices][name]
196
+ logger.info("Resolved package dependencies for `#{name}', " +
197
+ "to: #{dependencies.pretty_inspect}")
198
+ end
199
+ end
200
+
201
+ # Finds all package definitions in the manifest and sorts them into two
202
+ # buckets: new and existing packages, then creates new packages and points
203
+ # current release version to the existing packages.
204
+ # @return [void]
205
+ def process_packages
206
+ logger.info("Checking for new packages in release")
207
+
208
+ new_packages = []
209
+ existing_packages = []
210
+
211
+ @manifest["packages"].each do |package_meta|
212
+ filter = {:sha1 => package_meta["sha1"]}
213
+ if package_meta["fingerprint"]
214
+ filter[:fingerprint] = package_meta["fingerprint"]
215
+ filter = filter.sql_or
216
+ end
217
+
218
+ # Checking whether we might have the same bits somewhere
219
+ packages = Models::Package.where(filter).all
220
+
221
+ if packages.empty?
222
+ new_packages << package_meta
223
+ next
224
+ end
225
+
226
+ # Rebase is an interesting use case: we don't really care about
227
+ # preserving the original package/job versions, so if we have a
228
+ # checksum/fingerprint match, we can just substitute the original
229
+ # package/job version with an existing one.
230
+ if @rebase
231
+ substitutes = packages.select do |package|
232
+ package.release_id == @release_model.id &&
233
+ package.name == package_meta["name"] &&
234
+ package.dependency_set == Set.new(package_meta["dependencies"])
235
+ end
236
+
237
+ substitute = pick_best(substitutes, package_meta["version"])
238
+
239
+ if substitute
240
+ package_meta["version"] = substitute.version
241
+ package_meta["sha1"] = substitute.sha1
242
+ existing_packages << [substitute, package_meta]
243
+ next
244
+ end
245
+ end
246
+
247
+ # We can reuse an existing package as long as it
248
+ # belongs to the same release and has the same name and version.
249
+ existing_package = packages.find do |package|
250
+ package.release_id == @release_model.id &&
251
+ package.name == package_meta["name"] &&
252
+ package.version == package_meta["version"]
253
+ # NOT checking dependencies here b/c dependency change would
254
+ # bump the package version anyway.
255
+ end
256
+
257
+ if existing_package
258
+ existing_packages << [existing_package, package_meta]
259
+ else
260
+ # We found a package with the same checksum but different
261
+ # (release, name, version) tuple, so we need to make a copy
262
+ # of the package blob and create a new db entry for it
263
+ package = packages.first
264
+ package_meta["blobstore_id"] = package.blobstore_id
265
+ new_packages << package_meta
266
+ end
267
+ end
268
+
269
+ create_packages(new_packages)
270
+ use_existing_packages(existing_packages)
271
+ end
272
+
273
+ # Creates packages using provided metadata
274
+ # @param [Array<Hash>] packages Packages metadata
275
+ # @return [void]
276
+ def create_packages(packages)
277
+ if packages.empty?
278
+ @packages_unchanged = true
279
+ return
280
+ end
281
+
282
+ event_log.begin_stage("Creating new packages", packages.size)
283
+ packages.each do |package_meta|
284
+ package_desc = "#{package_meta["name"]}/#{package_meta["version"]}"
285
+ event_log.track(package_desc) do
286
+ logger.info("Creating new package `#{package_desc}'")
287
+ package = create_package(package_meta)
288
+ register_package(package)
289
+ end
290
+ end
291
+ end
292
+
293
+ # Points release DB model to existing packages described by given metadata
294
+ # @param [Array<Array>] packages Existing packages metadata
295
+ def use_existing_packages(packages)
296
+ return if packages.empty?
297
+
298
+ n_packages = packages.size
299
+ event_log.begin_stage("Processing #{n_packages} existing " +
300
+ "package#{n_packages > 1 ? "s" : ""}", 1)
301
+
302
+ event_log.track("Verifying checksums") do
303
+ packages.each do |package, package_meta|
304
+ package_desc = "#{package.name}/#{package.version}"
305
+ logger.info("Package `#{package_desc}' already exists, " +
306
+ "verifying checksum")
307
+
308
+ expected = package.sha1
309
+ received = package_meta["sha1"]
310
+
311
+ if expected != received
312
+ raise ReleaseExistingPackageHashMismatch,
313
+ "`#{package_desc}' checksum mismatch, " +
314
+ "expected #{expected} but received #{received}"
315
+ end
316
+ logger.info("Package `#{package_desc}' verified")
317
+ register_package(package)
318
+ end
319
+ end
320
+ end
321
+
322
+ # Creates package in DB according to given metadata
323
+ # @param [Hash] package_meta Package metadata
324
+ # @return [void]
325
+ def create_package(package_meta)
326
+ name, version = package_meta["name"], package_meta["version"]
327
+
328
+ package_attrs = {
329
+ :release => @release_model,
330
+ :name => name,
331
+ :sha1 => package_meta["sha1"],
332
+ :fingerprint => package_meta["fingerprint"],
333
+ :version => version
334
+ }
335
+
336
+ if @rebase
337
+ new_version = next_package_version(name, version)
338
+ if new_version != version
339
+ transition = "#{version} -> #{new_version}"
340
+ logger.info("Package `#{name}' rebased: #{transition}")
341
+ package_attrs[:version] = new_version
342
+ version = new_version
343
+ @package_rebase_mapping[name] = transition
344
+ end
345
+ end
346
+
347
+ package = Models::Package.new(package_attrs)
348
+ package.dependency_set = package_meta["dependencies"]
349
+
350
+ existing_blob = package_meta["blobstore_id"]
351
+ desc = "package `#{name}/#{version}'"
352
+
353
+ if existing_blob
354
+ logger.info("Creating #{desc} from existing blob #{existing_blob}")
355
+ package.blobstore_id = BlobUtil.copy_blob(existing_blob)
356
+ else
357
+ logger.info("Creating #{desc} from provided bits")
358
+
359
+ package_tgz = File.join(@tmp_release_dir, "packages", "#{name}.tgz")
360
+ result = Bosh::Exec.sh("tar -tzf #{package_tgz} 2>&1", :on_error => :return)
361
+ if result.failed?
362
+ logger.error("Extracting #{desc} archive failed, " +
363
+ "tar returned #{result.exit_status}, " +
364
+ "output: #{result.output}")
365
+ raise PackageInvalidArchive, "Extracting #{desc} archive failed. Check task debug log for details."
366
+ end
367
+
368
+ package.blobstore_id = BlobUtil.create_blob(package_tgz)
369
+ end
370
+
371
+ package.save
372
+ end
373
+
374
+ # Marks package model as used by release version model
375
+ # @param [Models::Package] package Package model
376
+ # @return [void]
377
+ def register_package(package)
378
+ @packages[package.name] = package
379
+ @release_version_model.add_package(package)
380
+ end
381
+
382
+ # Finds job template definitions in release manifest and sorts them into
383
+ # two buckets: new and existing job templates, then creates new job
384
+ # template records in the database and points release version to existing
385
+ # ones.
386
+ # @return [void]
387
+ def process_jobs
388
+ logger.info("Checking for new jobs in release")
389
+
390
+ new_jobs = []
391
+ existing_jobs = []
392
+
393
+ @manifest["jobs"].each do |job_meta|
394
+ filter = {:sha1 => job_meta["sha1"]}
395
+ if job_meta["fingerprint"]
396
+ filter[:fingerprint] = job_meta["fingerprint"]
397
+ filter = filter.sql_or
398
+ end
399
+
400
+ # Checking whether we might have the same bits somewhere
401
+ jobs = Models::Template.where(filter).all
402
+
403
+ if @rebase
404
+ substitutes = jobs.select do |job|
405
+ job.release_id == @release_model.id &&
406
+ job.name == job_meta["name"]
407
+ end
408
+
409
+ substitute = pick_best(substitutes, job_meta["version"])
410
+
411
+ if substitute
412
+ job_meta["version"] = substitute.version
413
+ job_meta["sha1"] = substitute.sha1
414
+ existing_jobs << [substitute, job_meta]
415
+ else
416
+ new_jobs << job_meta
417
+ end
418
+
419
+ next
420
+ end
421
+
422
+ template = jobs.find do |job|
423
+ job.release_id == @release_model.id &&
424
+ job.name == job_meta["name"] &&
425
+ job.version == job_meta["version"]
426
+ end
427
+
428
+ if template.nil?
429
+ new_jobs << job_meta
430
+ else
431
+ existing_jobs << [template, job_meta]
432
+ end
433
+ end
434
+
435
+ create_jobs(new_jobs)
436
+ use_existing_jobs(existing_jobs)
437
+ end
438
+
439
+ def create_jobs(jobs)
440
+ if jobs.empty?
441
+ @jobs_unchanged = true
442
+ return
443
+ end
444
+
445
+ event_log.begin_stage("Creating new jobs", jobs.size)
446
+ jobs.each do |job_meta|
447
+ job_desc = "#{job_meta["name"]}/#{job_meta["version"]}"
448
+ event_log.track(job_desc) do
449
+ logger.info("Creating new template `#{job_desc}'")
450
+ template = create_job(job_meta)
451
+ register_template(template)
452
+ end
453
+ end
454
+ end
455
+
456
+ def create_job(job_meta)
457
+ name, version = job_meta["name"], job_meta["version"]
458
+
459
+ template_attrs = {
460
+ :release => @release_model,
461
+ :name => name,
462
+ :sha1 => job_meta["sha1"],
463
+ :fingerprint => job_meta["fingerprint"],
464
+ :version => version
465
+ }
466
+
467
+ if @rebase
468
+ new_version = next_template_version(name, version)
469
+ if new_version != version
470
+ transition = "#{version} -> #{new_version}"
471
+ logger.info("Job `#{name}' rebased: #{transition}")
472
+ template_attrs[:version] = new_version
473
+ version = new_version
474
+ @job_rebase_mapping[name] = transition
475
+ end
476
+ end
477
+
478
+ logger.info("Creating job template `#{name}/#{version}' " +
479
+ "from provided bits")
480
+ template = Models::Template.new(template_attrs)
481
+
482
+ job_tgz = File.join(@tmp_release_dir, "jobs", "#{name}.tgz")
483
+ job_dir = File.join(@tmp_release_dir, "jobs", "#{name}")
484
+
485
+ FileUtils.mkdir_p(job_dir)
486
+
487
+ desc = "job `#{name}/#{version}'"
488
+ result = Bosh::Exec.sh("tar -C #{job_dir} -xzf #{job_tgz} 2>&1", :on_error => :return)
489
+ if result.failed?
490
+ logger.error("Extracting #{desc} archive failed in dir #{job_dir}, " +
491
+ "tar returned #{result.exit_status}, " +
492
+ "output: #{result.output}")
493
+ raise JobInvalidArchive, "Extracting #{desc} archive failed. Check task debug log for details."
494
+ end
495
+
496
+ manifest_file = File.join(job_dir, "job.MF")
497
+ unless File.file?(manifest_file)
498
+ raise JobMissingManifest,
499
+ "Missing job manifest for `#{template.name}'"
500
+ end
501
+
502
+ job_manifest = Psych.load_file(manifest_file)
503
+
504
+ if job_manifest["templates"]
505
+ job_manifest["templates"].each_key do |relative_path|
506
+ path = File.join(job_dir, "templates", relative_path)
507
+ unless File.file?(path)
508
+ raise JobMissingTemplateFile,
509
+ "Missing template file `#{relative_path}' " +
510
+ "for job `#{template.name}'"
511
+ end
512
+ end
513
+ end
514
+
515
+ main_monit_file = File.join(job_dir, "monit")
516
+ aux_monit_files = Dir.glob(File.join(job_dir, "*.monit"))
517
+
518
+ unless File.exists?(main_monit_file) || aux_monit_files.size > 0
519
+ raise JobMissingMonit, "Job `#{template.name}' is missing monit file"
520
+ end
521
+
522
+ template.blobstore_id = BlobUtil.create_blob(job_tgz)
523
+
524
+ package_names = []
525
+ job_manifest["packages"].each do |package_name|
526
+ package = @packages[package_name]
527
+ if package.nil?
528
+ raise JobMissingPackage,
529
+ "Job `#{template.name}' is referencing " +
530
+ "a missing package `#{package_name}'"
531
+ end
532
+ package_names << package.name
533
+ end
534
+ template.package_names = package_names
535
+
536
+ if job_manifest["logs"]
537
+ unless job_manifest["logs"].is_a?(Hash)
538
+ raise JobInvalidLogSpec,
539
+ "Job `#{template.name}' has invalid logs spec format"
540
+ end
541
+
542
+ template.logs = job_manifest["logs"]
543
+ end
544
+
545
+ if job_manifest["properties"]
546
+ unless job_manifest["properties"].is_a?(Hash)
547
+ raise JobInvalidPropertySpec,
548
+ "Job `#{template.name}' has invalid properties spec format"
549
+ end
550
+
551
+ template.properties = job_manifest["properties"]
552
+ end
553
+
554
+ template.save
555
+ end
556
+
557
+ # @param [Array<Array>] jobs Existing jobs metadata
558
+ # @return [void]
559
+ def use_existing_jobs(jobs)
560
+ return if jobs.empty?
561
+
562
+ n_jobs = jobs.size
563
+ event_log.begin_stage("Processing #{n_jobs} existing " +
564
+ "job#{n_jobs > 1 ? "s" : ""}", 1)
565
+
566
+ event_log.track("Verifying checksums") do
567
+ jobs.each do |template, job_meta|
568
+ job_desc = "#{template.name}/#{template.version}"
569
+
570
+ logger.info("Job `#{job_desc}' already exists, " +
571
+ "verifying checksum")
572
+
573
+ expected = template.sha1
574
+ received = job_meta["sha1"]
575
+
576
+ if expected != received
577
+ raise ReleaseExistingJobHashMismatch,
578
+ "`#{job_desc}' checksum mismatch, " +
579
+ "expected #{expected} but received #{received}"
580
+ end
581
+
582
+ logger.info("Job `#{job_desc}' verified")
583
+ register_template(template)
584
+ end
585
+ end
586
+ end
587
+
588
+ # Marks job template model as being used by release version
589
+ # @param [Models::Template] template Job template model
590
+ # @return [void]
591
+ def register_template(template)
592
+ @release_version_model.add_template(template)
593
+ end
594
+
595
+ private
596
+
597
+ # Returns the next release version (to be used for rebased release)
598
+ # @return [String]
599
+ def next_release_version
600
+ attrs = {
601
+ :release_id => @release_model.id
602
+ }
603
+ next_version(Models::ReleaseVersion.filter(attrs).all, @version)
604
+ end
605
+
606
+ # Returns the next package version (to be used for rebased package)
607
+ # @param [String] name Package name
608
+ # @param [String] version Package version
609
+ # @return [String]
610
+ def next_package_version(name, version)
611
+ attrs = {
612
+ :release_id => @release_model.id,
613
+ :name => name
614
+ }
615
+
616
+ next_version(Models::Package.filter(attrs).all, version)
617
+ end
618
+
619
+ # Returns the next job template version (to be used for rebased template)
620
+ # @param [String] name Template name
621
+ # @param [Fixnum] version Template version
622
+ # @return [String]
623
+ def next_template_version(name, version)
624
+ attrs = {
625
+ :release_id => @release_model.id,
626
+ :name => name
627
+ }
628
+
629
+ next_version(Models::Template.filter(attrs).all, version)
630
+ end
631
+
632
+ # Takes collection of versioned items and returns the version
633
+ # that new item should be promoted to if auto-versioning is used
634
+ # @param [Array<#version>] Collection of items
635
+ # @param [String] version Current version of item
636
+ # @return [String] Next version to be used
637
+ def next_version(collection, version)
638
+ Bosh::Director::NextRebaseVersion.new(collection).calculate(version)
639
+ end
640
+
641
+ # Removes release version model, along with all packages and templates.
642
+ # @return [void]
643
+ def remove_release_version_model
644
+ return unless @release_version_model && !@release_version_model.new?
645
+
646
+ @release_version_model.remove_all_packages
647
+ @release_version_model.remove_all_templates
648
+ @release_version_model.destroy
649
+ end
650
+
651
+ # Picks the best matching package/job from collection based on a simple
652
+ # heuristics: items with same version are preferred, then items
653
+ # with non-dev versions, then items with most compiled packages,
654
+ # then everything else.
655
+ # @param [Array<#name,#version>] collection
656
+ # @param [String] original_version
657
+ def pick_best(collection, original_version)
658
+ collection.sort_by { |item|
659
+ if item.version == original_version
660
+ 1
661
+ elsif Bosh::Common::VersionNumber.new(item.version).final?
662
+ 2
663
+ elsif item.is_a?(Bosh::Director::Models::Package)
664
+ 3000 - [item.compiled_packages.size, 900].min
665
+ else
666
+ 3000
667
+ end
668
+ }.first
669
+ end
670
+ end
671
+ end
672
+ end