bosh-director 1.5.0.pre.1113

Sign up to get free protection for your applications and to get access to all the features.
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