foreman_rh_cloud 3.0.21 → 3.0.24

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/api/v2/rh_cloud/inventory_controller.rb +50 -0
  3. data/app/controllers/concerns/inventory_upload/report_actions.rb +26 -0
  4. data/app/controllers/concerns/inventory_upload/task_actions.rb +25 -0
  5. data/app/controllers/foreman_inventory_upload/reports_controller.rb +3 -1
  6. data/app/controllers/foreman_inventory_upload/tasks_controller.rb +5 -13
  7. data/app/controllers/foreman_inventory_upload/uploads_controller.rb +4 -4
  8. data/app/controllers/insights_cloud/api/machine_telemetries_controller.rb +1 -0
  9. data/app/controllers/insights_cloud/hits_controller.rb +7 -3
  10. data/app/helpers/foreman_insights_deprecations_helper.rb +9 -0
  11. data/app/helpers/foreman_insights_host_helper.rb +19 -0
  12. data/app/models/insights_client_report_status.rb +4 -0
  13. data/app/overrides/old_plugin_deprecation.rb +20 -0
  14. data/app/services/foreman_rh_cloud/cloud_auth.rb +12 -0
  15. data/app/services/foreman_rh_cloud/cloud_request.rb +14 -0
  16. data/app/services/foreman_rh_cloud/cloud_request_forwarder.rb +1 -14
  17. data/app/services/foreman_rh_cloud/remediations_retriever.rb +1 -4
  18. data/config/package-lock.json.plugin +32774 -0
  19. data/config/routes.rb +19 -0
  20. data/lib/foreman_inventory_upload.rb +9 -1
  21. data/lib/foreman_inventory_upload/generators/fact_helpers.rb +19 -0
  22. data/lib/foreman_inventory_upload/generators/queries.rb +1 -0
  23. data/lib/foreman_inventory_upload/generators/slice.rb +7 -6
  24. data/lib/foreman_rh_cloud/engine.rb +19 -8
  25. data/lib/foreman_rh_cloud/version.rb +1 -1
  26. data/lib/insights_cloud/async/insights_client_status_aging.rb +17 -0
  27. data/lib/insights_cloud/async/insights_full_sync.rb +4 -14
  28. data/lib/insights_cloud/async/insights_resolutions_sync.rb +1 -4
  29. data/lib/insights_cloud/async/insights_rules_sync.rb +2 -7
  30. data/lib/inventory_sync/async/host_result.rb +4 -0
  31. data/lib/inventory_sync/async/inventory_full_sync.rb +2 -1
  32. data/lib/inventory_sync/async/inventory_hosts_sync.rb +16 -2
  33. data/lib/inventory_sync/async/inventory_scheduled_sync.rb +12 -0
  34. data/lib/inventory_sync/async/inventory_self_host_sync.rb +30 -0
  35. data/lib/inventory_sync/async/query_inventory_job.rb +6 -5
  36. data/lib/tasks/rh_cloud_inventory.rake +8 -1
  37. data/package.json +1 -1
  38. data/test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb +41 -0
  39. data/test/controllers/inventory_upload/api/inventory_controller_test.rb +53 -0
  40. data/test/factories/inventory_upload_factories.rb +1 -1
  41. data/test/jobs/insights_client_status_aging_test.rb +33 -0
  42. data/test/jobs/insights_full_sync_test.rb +3 -0
  43. data/test/jobs/insights_resolutions_sync_test.rb +3 -0
  44. data/test/jobs/insights_rules_sync_test.rb +3 -0
  45. data/test/jobs/inventory_full_sync_test.rb +3 -0
  46. data/test/jobs/inventory_hosts_sync_test.rb +267 -0
  47. data/test/jobs/inventory_scheduled_sync_test.rb +22 -0
  48. data/test/jobs/inventory_self_host_sync_test.rb +103 -0
  49. data/test/test_plugin_helper.rb +2 -0
  50. data/test/unit/services/foreman_rh_cloud/cloud_request_forwarder_test.rb +3 -3
  51. data/test/unit/slice_generator_test.rb +81 -29
  52. data/webpack/ForemanInventoryUpload/Components/FullScreenModal/FullScreenModal.js +1 -1
  53. data/webpack/ForemanInventoryUpload/Components/FullScreenModal/__tests__/__snapshots__/FullScreenModal.test.js.snap +1 -1
  54. data/webpack/ForemanInventoryUpload/Components/FullScreenModal/fullScreenModal.scss +14 -16
  55. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/PageDescription/PageDescription.js +11 -0
  56. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/PageDescription/__tests__/__snapshots__/PageDescription.test.js.snap +11 -0
  57. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/SyncButtonActions.js +28 -63
  58. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/__snapshots__/integrations.test.js.snap +2 -3
  59. data/webpack/ForemanInventoryUpload/Components/Terminal/Terminal.js +1 -1
  60. data/webpack/ForemanInventoryUpload/Components/Terminal/__tests__/Terminal.test.js +1 -1
  61. data/webpack/ForemanInventoryUpload/Components/Terminal/__tests__/__snapshots__/Terminal.test.js.snap +2 -2
  62. data/webpack/ForemanInventoryUpload/Components/Terminal/terminal.scss +25 -27
  63. data/webpack/InsightsCloudSync/Components/InsightsTable/InsightsTableActions.js +19 -19
  64. data/webpack/InsightsCloudSync/Components/InsightsTable/__tests__/__snapshots__/InsightsTableActions.test.js.snap +14 -14
  65. data/webpack/InsightsCloudSync/Components/RemediationModal/RemediateButton.js +1 -0
  66. data/webpack/InsightsCloudSync/Components/__tests__/__snapshots__/NoTokenEmptyState.test.js.snap +20 -13
  67. data/webpack/InsightsCloudSync/InsightsCloudSync.js +4 -1
  68. data/webpack/InsightsCloudSync/InsightsCloudSyncActions.js +44 -20
  69. data/webpack/InsightsCloudSync/InsightsCloudSyncConstants.js +2 -0
  70. data/webpack/InsightsCloudSync/__snapshots__/InsightsCloudSync.test.js.snap +2 -2
  71. data/webpack/InsightsCloudSync/__tests__/__snapshots__/InsightsCloudSyncActions.test.js.snap +11 -7
  72. data/webpack/common/ForemanTasks/ForemanTasksActions.js +64 -0
  73. data/webpack/common/ForemanTasks/ForemanTasksHelpers.js +7 -0
  74. data/webpack/common/ForemanTasks/index.js +1 -0
  75. metadata +25 -2
data/config/routes.rb CHANGED
@@ -40,4 +40,23 @@ Rails.application.routes.draw do
40
40
  match '/*path', :constraints => lambda { |req| !req.path.include?('view/api') }, to: 'machine_telemetries#forward_request', via: [:get, :post, :delete,:put, :patch]
41
41
  end
42
42
  end
43
+
44
+ # API routes
45
+
46
+ namespace :api, :defaults => {:format => 'json'} do
47
+ scope '(:apiv)', :module => :v2, :defaults => {:apiv => 'v2'}, :apiv => /v1|v2/, :constraints => ApiConstraints.new(:version => 2, :default => true) do
48
+ resources :organizations, :only => [:show] do
49
+ namespace 'rh_cloud' do
50
+ get 'report', to: 'inventory#download_file'
51
+ post 'report', to: 'inventory#generate_report'
52
+
53
+ post 'inventory_sync', to: 'inventory#sync_inventory_status'
54
+ end
55
+ end
56
+
57
+ namespace 'rh_cloud' do
58
+ post 'enable_connector', to: 'inventory#enable_cloud_connector'
59
+ end
60
+ end
61
+ end
43
62
  end
@@ -70,8 +70,16 @@ module ForemanInventoryUpload
70
70
  folder
71
71
  end
72
72
 
73
+ def self.inventory_base_url
74
+ ForemanRhCloud.base_url + "/api/inventory/v1/hosts"
75
+ end
76
+
73
77
  def self.inventory_export_url
74
78
  tags = URI.encode("satellite/satellite_instance_id=#{Foreman.instance_id}")
75
- ForemanRhCloud.base_url + "/api/inventory/v1/hosts?tags=#{tags}"
79
+ inventory_base_url + "?tags=#{tags}"
80
+ end
81
+
82
+ def self.inventory_self_url
83
+ inventory_base_url + "?hostname_or_id=#{ForemanRhCloud.foreman_host.fqdn}"
76
84
  end
77
85
  end
@@ -10,6 +10,8 @@ module ForemanInventoryUpload
10
10
  CLOUD_AZURE = 'azure'
11
11
  CLOUD_ALIBABA = 'alibaba'
12
12
 
13
+ UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
14
+
13
15
  def fact_value(host, fact_name)
14
16
  value_record = host.fact_values.find do |fact_value|
15
17
  fact_value.fact_name_id == ForemanInventoryUpload::Generators::Queries.fact_names[fact_name]
@@ -104,6 +106,23 @@ module ForemanInventoryUpload
104
106
  def obfuscate_ip(ip, ips_dict)
105
107
  "10.230.230.#{ips_dict.count + 1}"
106
108
  end
109
+
110
+ def bios_uuid(host)
111
+ value = fact_value(host, 'dmi::system::uuid') || ''
112
+ uuid_value(value)
113
+ end
114
+
115
+ def uuid_value(value)
116
+ uuid_match = UUID_REGEX.match(value)
117
+ uuid_match&.to_s
118
+ end
119
+
120
+ def uuid_value!(value)
121
+ uuid = uuid_value(value)
122
+ raise Foreman::Exception.new(N_('Value %{value} is not a valid UUID') % {value: value}) if value && uuid.empty?
123
+
124
+ uuid
125
+ end
107
126
  end
108
127
  end
109
128
  end
@@ -27,6 +27,7 @@ module ForemanInventoryUpload
27
27
  'insights_client::obfuscate_ip_enabled',
28
28
  'insights_client::hostname',
29
29
  'insights_client::ips',
30
+ 'insights_id',
30
31
  ]).pluck(:name, :id)
31
32
  ]
32
33
  end
@@ -25,7 +25,7 @@ module ForemanInventoryUpload
25
25
 
26
26
  def report_slice(hosts_batch)
27
27
  @stream.object do
28
- @stream.simple_field('report_slice_id', @slice_id)
28
+ @stream.simple_field('report_slice_id', uuid_value!(@slice_id))
29
29
  @stream.array_field('hosts', :last) do
30
30
  first = true
31
31
  hosts_batch.each do |host|
@@ -45,10 +45,11 @@ module ForemanInventoryUpload
45
45
  @stream.object do
46
46
  @stream.simple_field('fqdn', fqdn(host))
47
47
  @stream.simple_field('account', account_id(host.organization).to_s)
48
- @stream.simple_field('subscription_manager_id', host.subscription_facet&.uuid)
49
- @stream.simple_field('satellite_id', host.subscription_facet&.uuid)
50
- @stream.simple_field('bios_uuid', fact_value(host, 'dmi::system::uuid'))
51
- @stream.simple_field('vm_uuid', fact_value(host, 'virt::uuid'))
48
+ @stream.simple_field('subscription_manager_id', uuid_value!(host.subscription_facet&.uuid))
49
+ @stream.simple_field('satellite_id', uuid_value!(host.subscription_facet&.uuid))
50
+ @stream.simple_field('bios_uuid', bios_uuid(host))
51
+ @stream.simple_field('vm_uuid', uuid_value(fact_value(host, 'virt::uuid')))
52
+ @stream.simple_field('insights_id', uuid_value(fact_value(host, 'insights_id')))
52
53
  report_ip_addresses(host, host_ips_cache)
53
54
  report_mac_addresses(host)
54
55
  @stream.object_field('system_profile') do
@@ -85,7 +86,7 @@ module ForemanInventoryUpload
85
86
  @stream.object do
86
87
  @stream.simple_field('namespace', namespace)
87
88
  @stream.simple_field('key', key)
88
- @stream.simple_field('value', value, :last)
89
+ @stream.simple_field('value', value.to_s, :last)
89
90
  end
90
91
  @stream.comma unless last
91
92
  end
@@ -6,6 +6,18 @@ module ForemanRhCloud
6
6
  class Engine < ::Rails::Engine
7
7
  engine_name 'foreman_rh_cloud'
8
8
 
9
+ def self.register_scheduled_task(task_class, cronline)
10
+ return if ForemanTasks::RecurringLogic.joins(:tasks)
11
+ .merge(ForemanTasks::Task.where(label: task_class.name))
12
+ .exists?
13
+
14
+ User.as_anonymous_admin do
15
+ recurring_logic = ForemanTasks::RecurringLogic.new_from_cronline(cronline)
16
+ recurring_logic.save!
17
+ recurring_logic.start(task_class)
18
+ end
19
+ end
20
+
9
21
  initializer 'foreman_rh_cloud.load_default_settings', :before => :load_config_initializers do
10
22
  require_dependency File.expand_path('../../app/models/setting/rh_cloud.rb', __dir__)
11
23
  end
@@ -83,6 +95,10 @@ module ForemanRhCloud
83
95
 
84
96
  subscribe 'host_created.event.foreman', ForemanRhCloud::InsightsSubscriber
85
97
 
98
+ describe_host do
99
+ overview_buttons_provider :insights_host_overview_buttons
100
+ end
101
+
86
102
  extend_page 'hosts/show' do |context|
87
103
  context.add_pagelet :main_tabs,
88
104
  partial: 'hosts/insights_tab',
@@ -133,14 +149,9 @@ module ForemanRhCloud
133
149
  )
134
150
  # skip object creation when admin user is not present, for example in test DB
135
151
  if User.unscoped.find_by_login(User::ANONYMOUS_ADMIN).present?
136
- unless ForemanTasks::RecurringLogic.joins(:tasks).merge(
137
- ForemanTasks::Task.where(label: 'InventorySync::Async::InventoryScheduledSync')
138
- ).exists?
139
- User.as_anonymous_admin do
140
- recurring_logic = ForemanTasks::RecurringLogic.new_from_cronline("0 0 * * *")
141
- recurring_logic.save!
142
- recurring_logic.start(InventorySync::Async::InventoryScheduledSync)
143
- end
152
+ ::ForemanTasks.dynflow.config.on_init(false) do |world|
153
+ ForemanRhCloud::Engine.register_scheduled_task(InventorySync::Async::InventoryScheduledSync, '0 0 * * *')
154
+ ForemanRhCloud::Engine.register_scheduled_task(InsightsCloud::Async::InsightsClientStatusAging, '0 0 * * *')
144
155
  end
145
156
  end
146
157
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '3.0.21'.freeze
2
+ VERSION = '3.0.24'.freeze
3
3
  end
@@ -0,0 +1,17 @@
1
+ module InsightsCloud
2
+ module Async
3
+ class InsightsClientStatusAging < ::Actions::EntryAction
4
+ include ::Actions::RecurringAction
5
+
6
+ def run
7
+ # update all stale records to "not reporting" counterpart
8
+ InsightsClientReportStatus.stale.reporting.update_all(status: InsightsClientReportStatus::NO_REPORT)
9
+ InsightsClientReportStatus.stale.not_managed_with_data.update_all(status: InsightsClientReportStatus::NOT_MANAGED)
10
+ end
11
+
12
+ def logger
13
+ action_logger
14
+ end
15
+ end
16
+ end
17
+ end
@@ -50,28 +50,18 @@ module InsightsCloud
50
50
  end
51
51
 
52
52
  def query_insights_hits
53
- hits_response = RestClient::Request.execute(
53
+ hits_response = execute_cloud_request(
54
54
  method: :get,
55
- url: InsightsCloud.hits_export_url,
56
- verify_ssl: ForemanRhCloud.verify_ssl_method,
57
- proxy: ForemanRhCloud.transformed_http_proxy_string(logger: logger),
58
- headers: {
59
- Authorization: "Bearer #{rh_credentials}",
60
- }
55
+ url: InsightsCloud.hits_export_url
61
56
  )
62
57
 
63
58
  JSON.parse(hits_response)
64
59
  end
65
60
 
66
61
  def query_insights_rules
67
- rules_response = RestClient::Request.execute(
62
+ rules_response = execute_cloud_request(
68
63
  method: :get,
69
- url: InsightsCloud.rules_url,
70
- verify_ssl: ForemanRhCloud.verify_ssl_method,
71
- proxy: ForemanRhCloud.transformed_http_proxy_string(logger: logger),
72
- headers: {
73
- Authorization: "Bearer #{rh_credentials}",
74
- }
64
+ url: InsightsCloud.rules_url
75
65
  )
76
66
 
77
67
  JSON.parse(rules_response)
@@ -22,14 +22,11 @@ module InsightsCloud
22
22
  private
23
23
 
24
24
  def query_insights_resolutions(rule_ids)
25
- resolutions_response = RestClient::Request.execute(
25
+ resolutions_response = execute_cloud_request(
26
26
  method: :post,
27
27
  url: InsightsCloud.resolutions_url,
28
- verify_ssl: ForemanRhCloud.verify_ssl_method,
29
- proxy: ForemanRhCloud.transformed_http_proxy_string(logger: logger),
30
28
  headers: {
31
29
  content_type: :json,
32
- Authorization: "Bearer #{rh_credentials}",
33
30
  },
34
31
  payload: {
35
32
  issues: rule_ids,
@@ -37,14 +37,9 @@ module InsightsCloud
37
37
  private
38
38
 
39
39
  def query_insights_rules(offset)
40
- rules_response = RestClient::Request.execute(
40
+ rules_response = execute_cloud_request(
41
41
  method: :get,
42
- url: InsightsCloud.rules_url(offset: offset),
43
- verify_ssl: ForemanRhCloud.verify_ssl_method,
44
- proxy: ForemanRhCloud.transformed_http_proxy_string(logger: logger),
45
- headers: {
46
- Authorization: "Bearer #{rh_credentials}",
47
- }
42
+ url: InsightsCloud.rules_url(offset: offset)
48
43
  )
49
44
 
50
45
  JSON.parse(rules_response)
@@ -1,6 +1,8 @@
1
1
  module InventorySync
2
2
  module Async
3
3
  class HostResult
4
+ attr_reader :uuid_by_fqdn
5
+
4
6
  def initialize(result)
5
7
  @total = result['total']
6
8
  @count = result['count']
@@ -8,6 +10,7 @@ module InventorySync
8
10
  @per_page = result['per_page']
9
11
  @sub_ids = result["results"].map { |host| host['subscription_manager_id'] }
10
12
  @uuid_by_sub_id = Hash[result["results"].map { |host| [host['subscription_manager_id'], host['id']] }]
13
+ @uuid_by_fqdn = Hash[result["results"].map { |host| [host['fqdn'].downcase, host['id']] }]
11
14
  end
12
15
 
13
16
  def status_hashes
@@ -19,6 +22,7 @@ module InventorySync
19
22
  host_id: host_id,
20
23
  status: InventorySync::InventoryStatus::SYNC,
21
24
  reported_at: DateTime.current,
25
+ type: InventorySync::InventoryStatus.name,
22
26
  }
23
27
  end
24
28
  end.compact
@@ -24,6 +24,7 @@ module InventorySync
24
24
 
25
25
  logger.debug("Synced hosts amount: #{host_statuses[:sync]}")
26
26
  logger.debug("Disconnected hosts amount: #{host_statuses[:disconnect]}")
27
+ output[:host_statuses] = host_statuses
27
28
  end
28
29
 
29
30
  def update_statuses_batch
@@ -53,7 +54,7 @@ module InventorySync
53
54
  end
54
55
 
55
56
  def host_statuses
56
- output[:host_statuses] ||= {
57
+ @host_statuses ||= {
57
58
  sync: 0,
58
59
  disconnect: 0,
59
60
  }
@@ -4,6 +4,12 @@ module InventorySync
4
4
  set_callback :iteration, :around, :setup_facet_transaction
5
5
  set_callback :step, :around, :create_facets
6
6
 
7
+ def plan
8
+ # by default the tasks will be executed concurrently
9
+ plan_self
10
+ plan_self_host_sync
11
+ end
12
+
7
13
  def setup_facet_transaction
8
14
  InsightsFacet.transaction do
9
15
  yield
@@ -20,14 +26,22 @@ module InventorySync
20
26
  private
21
27
 
22
28
  def add_missing_insights_facets(uuids_hash)
23
- existing_facets = InsightsFacet.where(host_id: uuids_hash.keys).pluck(:host_id)
24
- missing_facets = uuids_hash.except(*existing_facets).map do |host_id, uuid|
29
+ existing_facets = InsightsFacet.where(host_id: uuids_hash.keys).pluck(:host_id, :uuid)
30
+ missing_facets = uuids_hash.except(*existing_facets.map(&:first)).map do |host_id, uuid|
25
31
  {
26
32
  host_id: host_id,
27
33
  uuid: uuid,
28
34
  }
29
35
  end
30
36
  InsightsFacet.create(missing_facets)
37
+
38
+ existing_facets.select { |host_id, uuid| uuid.empty? }.each do |host_id, _uuid|
39
+ InsightsFacet.where(host_id: host_id).update_all(uuid: uuids_hash[host_id])
40
+ end
41
+ end
42
+
43
+ def plan_self_host_sync
44
+ plan_action InventorySync::Async::InventorySelfHostSync
31
45
  end
32
46
  end
33
47
  end
@@ -4,6 +4,14 @@ module InventorySync
4
4
  include ::Actions::RecurringAction
5
5
 
6
6
  def plan
7
+ unless Setting[:allow_auto_inventory_upload]
8
+ logger.debug(
9
+ 'The scheduled process is disabled due to the "allow_auto_inventory_upload"
10
+ setting being set to false.'
11
+ )
12
+ return
13
+ end
14
+
7
15
  Organization.unscoped.each do |org|
8
16
  plan_org_sync(org)
9
17
  end
@@ -12,6 +20,10 @@ module InventorySync
12
20
  def plan_org_sync(org)
13
21
  plan_action InventoryFullSync, org
14
22
  end
23
+
24
+ def logger
25
+ action_logger
26
+ end
15
27
  end
16
28
  end
17
29
  end
@@ -0,0 +1,30 @@
1
+ module InventorySync
2
+ module Async
3
+ class InventorySelfHostSync < QueryInventoryJob
4
+ set_callback :step, :around, :create_facets
5
+
6
+ def create_facets
7
+ # get the results from the event
8
+ results = yield
9
+
10
+ add_missing_insights_facet(results.uuid_by_fqdn) unless results.uuid_by_fqdn.empty?
11
+ results
12
+ end
13
+
14
+ private
15
+
16
+ def add_missing_insights_facet(uuids_hash)
17
+ facet = InsightsFacet.find_or_create_by(host_id: ForemanRhCloud.foreman_host.id) do |facet|
18
+ facet.uuid = uuids_hash.values.first
19
+ end
20
+
21
+ # fix empty uuid in case the facet already exists
22
+ facet.update(uuid: uuids_hash.values.first) unless facet.uuid
23
+ end
24
+
25
+ def request_url
26
+ ForemanInventoryUpload.inventory_self_url
27
+ end
28
+ end
29
+ end
30
+ end
@@ -29,13 +29,10 @@ module InventorySync
29
29
  private
30
30
 
31
31
  def query_inventory(page = 1)
32
- hosts_inventory_response = RestClient::Request.execute(
32
+ hosts_inventory_response = execute_cloud_request(
33
33
  method: :get,
34
- url: ForemanInventoryUpload.inventory_export_url,
35
- verify_ssl: ForemanRhCloud.verify_ssl_method,
36
- proxy: ForemanRhCloud.transformed_http_proxy_string(logger: logger),
34
+ url: request_url,
37
35
  headers: {
38
- Authorization: "Bearer #{rh_credentials}",
39
36
  params: {
40
37
  per_page: 100,
41
38
  page: page,
@@ -49,6 +46,10 @@ module InventorySync
49
46
  def logger
50
47
  action_logger
51
48
  end
49
+
50
+ def request_url
51
+ ForemanInventoryUpload.inventory_export_url
52
+ end
52
53
  end
53
54
  end
54
55
  end
@@ -23,8 +23,15 @@ namespace :rh_cloud_inventory do
23
23
  organizations = [ENV['organization_id']]
24
24
  base_folder = ENV['target'] || Dir.pwd
25
25
 
26
- unless portal_user || organizations.empty?
26
+ unless File.writable?(base_folder)
27
+ puts "#{base_folder} is not writable by the current process"
28
+ base_folder = Dir.mktmpdir
29
+ puts "Using #{base_folder} for the output"
30
+ end
31
+
32
+ if portal_user.empty? && organizations.empty?
27
33
  puts "Must specify either portal_user or organization_id"
34
+ return
28
35
  end
29
36
 
30
37
  User.as_anonymous_admin do