blob: 1ffe9723a3660667fb9adf9346f9b83dad352483 [file] [log] [blame]
Matteo Scandoloeb0d11c2017-08-08 13:05:26 -07001
2# Copyright 2017-present Open Networking Foundation
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16
Scott Baker31acc652016-06-23 15:47:56 -070017from django.contrib import admin
18
Srikanth Vavilapallid84b7b72016-06-28 00:19:07 +000019from services.monitoring.models import *
Scott Baker31acc652016-06-23 15:47:56 -070020from django import forms
21from django.utils.safestring import mark_safe
22from django.contrib.auth.admin import UserAdmin
23from django.contrib.admin.widgets import FilteredSelectMultiple
24from django.contrib.auth.forms import ReadOnlyPasswordHashField
25from django.contrib.auth.signals import user_logged_in
26from django.utils import timezone
27from django.contrib.contenttypes import generic
28from suit.widgets import LinkedSelect
rdudyala996d70b2016-10-13 17:40:55 +000029from core.admin import XOSBaseAdmin,ServiceAppAdmin,SliceInline,ServiceAttrAsTabInline, ReadOnlyAwareAdmin, XOSTabularInline, ServicePrivilegeInline, TenantRootTenantInline, TenantRootPrivilegeInline, TenantAttrAsTabInline, UploadTextareaWidget
Scott Baker31acc652016-06-23 15:47:56 -070030from core.middleware import get_request
31
32from functools import update_wrapper
33from django.contrib.admin.views.main import ChangeList
34from django.core.urlresolvers import reverse
35from django.contrib.admin.utils import quote
36
37class CeilometerServiceForm(forms.ModelForm):
38 ceilometer_pub_sub_url = forms.CharField(required=False, max_length=1024, help_text="REST URL of ceilometer PUB/SUB component in http://IP:port/ format")
Srikanth Vavilapalli71aa28d2017-01-31 00:43:13 +000039 ceilometer_enable_pub_sub = forms.BooleanField()
Scott Baker31acc652016-06-23 15:47:56 -070040
41 def __init__(self,*args,**kwargs):
42 super (CeilometerServiceForm,self ).__init__(*args,**kwargs)
43 if self.instance:
44 # fields for the attributes
45 self.fields['ceilometer_pub_sub_url'].initial = self.instance.ceilometer_pub_sub_url
Srikanth Vavilapalli71aa28d2017-01-31 00:43:13 +000046 self.fields['ceilometer_enable_pub_sub'].initial = self.instance.ceilometer_enable_pub_sub
Scott Baker31acc652016-06-23 15:47:56 -070047
48 def save(self, commit=True):
49 self.instance.ceilometer_pub_sub_url = self.cleaned_data.get("ceilometer_pub_sub_url")
Srikanth Vavilapalli71aa28d2017-01-31 00:43:13 +000050 self.instance.ceilometer_enable_pub_sub = self.cleaned_data.get("ceilometer_enable_pub_sub")
Scott Baker31acc652016-06-23 15:47:56 -070051 return super(CeilometerServiceForm, self).save(commit=commit)
52
53 class Meta:
54 model = CeilometerService
Zack Williams1861b522016-06-27 08:25:33 -070055 fields = '__all__'
Scott Baker31acc652016-06-23 15:47:56 -070056
57class CeilometerServiceAdmin(ReadOnlyAwareAdmin):
58 model = CeilometerService
59 verbose_name = "Ceilometer Service"
60 verbose_name_plural = "Ceilometer Service"
61 list_display = ("backend_status_icon", "name", "enabled")
62 list_display_links = ('backend_status_icon', 'name', )
Srikanth Vavilapalli71aa28d2017-01-31 00:43:13 +000063 fieldsets = [(None, {'fields': ['backend_status_text', 'name','enabled','versionNumber', 'description','ceilometer_pub_sub_url', 'ceilometer_enable_pub_sub', "view_url","icon_url" ], 'classes':['suit-tab suit-tab-general']})]
64 readonly_fields = ('backend_status_text', 'ceilometer_pub_sub_url',)
Scott Baker31acc652016-06-23 15:47:56 -070065 inlines = [SliceInline,ServiceAttrAsTabInline,ServicePrivilegeInline]
66 form = CeilometerServiceForm
67
68 extracontext_registered_admins = True
69
70 user_readonly_fields = ["name", "enabled", "versionNumber", "description"]
71
72 suit_form_tabs =(('general', 'Ceilometer Service Details'),
73 ('administration', 'Administration'),
74 ('slices','Slices'),
75 ('serviceattrs','Additional Attributes'),
76 ('serviceprivileges','Privileges'),
77 )
78
79 suit_form_includes = (('ceilometeradmin.html', 'top', 'administration'),
80 )
rdudyala996d70b2016-10-13 17:40:55 +000081 #actions=['delete_selected_objects']
82
83 #def get_actions(self, request):
84 # actions = super(CeilometerServiceAdmin, self).get_actions(request)
85 # if 'delete_selected' in actions:
86 # del actions['delete_selected']
87 # return actions
88
89 #def delete_selected_objects(self, request, queryset):
90 # for obj in queryset:
91 # obj.delete()
92 #delete_selected_objects.short_description = "Delete Selected Ceilometer Service Objects"
Scott Baker31acc652016-06-23 15:47:56 -070093
Zack Williams031e1b62016-06-27 13:19:50 -070094 def get_queryset(self, request):
Scott Baker31acc652016-06-23 15:47:56 -070095 return CeilometerService.get_service_objects_by_user(request.user)
96
97class MonitoringChannelForm(forms.ModelForm):
98 creator = forms.ModelChoiceField(queryset=User.objects.all())
99
100 def __init__(self,*args,**kwargs):
101 super (MonitoringChannelForm,self ).__init__(*args,**kwargs)
102 self.fields['kind'].widget.attrs['readonly'] = True
103 self.fields['provider_service'].queryset = CeilometerService.get_service_objects().all()
104 if self.instance:
105 # fields for the attributes
106 self.fields['creator'].initial = self.instance.creator
107 if (not self.instance) or (not self.instance.pk):
108 # default fields for an 'add' form
109 self.fields['kind'].initial = CEILOMETER_KIND
110 self.fields['creator'].initial = get_request().user
111 if CeilometerService.get_service_objects().exists():
112 self.fields["provider_service"].initial = CeilometerService.get_service_objects().all()[0]
113
114
115 def save(self, commit=True):
116 self.instance.creator = self.cleaned_data.get("creator")
117 return super(MonitoringChannelForm, self).save(commit=commit)
118
119 class Meta:
120 model = MonitoringChannel
Zack Williams1861b522016-06-27 08:25:33 -0700121 fields = '__all__'
Scott Baker31acc652016-06-23 15:47:56 -0700122
123class MonitoringChannelAdmin(ReadOnlyAwareAdmin):
124 list_display = ('backend_status_icon', 'id', )
125 list_display_links = ('backend_status_icon', 'id')
126 fieldsets = [ (None, {'fields': ['backend_status_text', 'kind', 'provider_service', 'service_specific_attribute',
Srikanth Vavilapalli71aa28d2017-01-31 00:43:13 +0000127 'ceilometer_url', 'ceilometer_ssh_proxy_url', 'kafka_url', 'tenant_list_str',
Scott Baker31acc652016-06-23 15:47:56 -0700128 'instance', 'creator'],
129 'classes':['suit-tab suit-tab-general']})]
Srikanth Vavilapalli71aa28d2017-01-31 00:43:13 +0000130 readonly_fields = ('backend_status_text', 'instance', 'service_specific_attribute', 'ceilometer_url', 'ceilometer_ssh_proxy_url', 'kafka_url', 'tenant_list_str')
Scott Baker31acc652016-06-23 15:47:56 -0700131 form = MonitoringChannelForm
132
133 suit_form_tabs = (('general','Details'),)
rdudyala996d70b2016-10-13 17:40:55 +0000134 #actions=['delete_selected_objects']
Scott Baker31acc652016-06-23 15:47:56 -0700135
rdudyala996d70b2016-10-13 17:40:55 +0000136 #def get_actions(self, request):
137 # actions = super(MonitoringChannelAdmin, self).get_actions(request)
138 # if 'delete_selected' in actions:
139 # del actions['delete_selected']
140 # return actions
Scott Baker31acc652016-06-23 15:47:56 -0700141
rdudyala996d70b2016-10-13 17:40:55 +0000142 #def delete_selected_objects(self, request, queryset):
143 # for obj in queryset:
144 # obj.delete()
145 #delete_selected_objects.short_description = "Delete Selected MonitoringChannel Objects"
Scott Baker31acc652016-06-23 15:47:56 -0700146
Zack Williams031e1b62016-06-27 13:19:50 -0700147 def get_queryset(self, request):
Scott Baker31acc652016-06-23 15:47:56 -0700148 return MonitoringChannel.get_tenant_objects_by_user(request.user)
149
150class SFlowServiceForm(forms.ModelForm):
151 sflow_port = forms.IntegerField(required=False)
152 sflow_api_port = forms.IntegerField(required=False)
153
154 def __init__(self,*args,**kwargs):
155 super (SFlowServiceForm,self ).__init__(*args,**kwargs)
156 if self.instance:
157 # fields for the attributes
158 self.fields['sflow_port'].initial = self.instance.sflow_port
159 self.fields['sflow_api_port'].initial = self.instance.sflow_api_port
160 if (not self.instance) or (not self.instance.pk):
161 # default fields for an 'add' form
162 self.fields['sflow_port'].initial = SFLOW_PORT
163 self.fields['sflow_api_port'].initial = SFLOW_API_PORT
164
165 def save(self, commit=True):
166 self.instance.sflow_port = self.cleaned_data.get("sflow_port")
167 self.instance.sflow_api_port = self.cleaned_data.get("sflow_api_port")
168 return super(SFlowServiceForm, self).save(commit=commit)
169
170 class Meta:
171 model = SFlowService
Zack Williams1861b522016-06-27 08:25:33 -0700172 fields = '__all__'
Scott Baker31acc652016-06-23 15:47:56 -0700173
174class SFlowServiceAdmin(ReadOnlyAwareAdmin):
175 model = SFlowService
176 verbose_name = "SFlow Service"
177 verbose_name_plural = "SFlow Service"
178 list_display = ("backend_status_icon", "name", "enabled")
179 list_display_links = ('backend_status_icon', 'name', )
180 fieldsets = [(None, {'fields': ['backend_status_text', 'name','enabled','versionNumber', 'description',"view_url","sflow_port","sflow_api_port","icon_url" ], 'classes':['suit-tab suit-tab-general']})]
181 readonly_fields = ('backend_status_text', )
182 inlines = [SliceInline,ServiceAttrAsTabInline,ServicePrivilegeInline]
183 form = SFlowServiceForm
184
185 extracontext_registered_admins = True
186
187 user_readonly_fields = ["name", "enabled", "versionNumber", "description"]
188
189 suit_form_tabs =(('general', 'SFlow Service Details'),
190 ('administration', 'Administration'),
191 ('slices','Slices'),
192 ('serviceattrs','Additional Attributes'),
193 ('serviceprivileges','Privileges'),
194 )
195
196 suit_form_includes = (('sflowadmin.html', 'top', 'administration'),
197 )
198
Zack Williams031e1b62016-06-27 13:19:50 -0700199 def get_queryset(self, request):
Scott Baker31acc652016-06-23 15:47:56 -0700200 return SFlowService.get_service_objects_by_user(request.user)
201
202class SFlowTenantForm(forms.ModelForm):
203 creator = forms.ModelChoiceField(queryset=User.objects.all())
204 listening_endpoint = forms.CharField(max_length=1024, help_text="sFlow listening endpoint in udp://IP:port format")
205
206 def __init__(self,*args,**kwargs):
207 super (SFlowTenantForm,self ).__init__(*args,**kwargs)
208 self.fields['kind'].widget.attrs['readonly'] = True
209 self.fields['provider_service'].queryset = SFlowService.get_service_objects().all()
210 if self.instance:
211 # fields for the attributes
212 self.fields['creator'].initial = self.instance.creator
213 self.fields['listening_endpoint'].initial = self.instance.listening_endpoint
214 if (not self.instance) or (not self.instance.pk):
215 # default fields for an 'add' form
216 self.fields['kind'].initial = SFLOW_KIND
217 self.fields['creator'].initial = get_request().user
218 if SFlowService.get_service_objects().exists():
219 self.fields["provider_service"].initial = SFlowService.get_service_objects().all()[0]
220
221 def save(self, commit=True):
222 self.instance.creator = self.cleaned_data.get("creator")
223 self.instance.listening_endpoint = self.cleaned_data.get("listening_endpoint")
224 return super(SFlowTenantForm, self).save(commit=commit)
225
226 class Meta:
227 model = SFlowTenant
Zack Williams1861b522016-06-27 08:25:33 -0700228 fields = '__all__'
Scott Baker31acc652016-06-23 15:47:56 -0700229
230class SFlowTenantAdmin(ReadOnlyAwareAdmin):
231 list_display = ('backend_status_icon', 'creator', 'listening_endpoint' )
232 list_display_links = ('backend_status_icon', 'listening_endpoint')
233 fieldsets = [ (None, {'fields': ['backend_status_text', 'kind', 'provider_service', 'subscriber_service', 'service_specific_attribute', 'listening_endpoint',
234 'creator'],
235 'classes':['suit-tab suit-tab-general']})]
236 readonly_fields = ('backend_status_text', 'instance', 'service_specific_attribute')
237 inlines = [TenantAttrAsTabInline]
238 form = SFlowTenantForm
239
240 suit_form_tabs = (('general','Details'), ('tenantattrs', 'Attributes'))
241
Zack Williams031e1b62016-06-27 13:19:50 -0700242 def get_queryset(self, request):
Scott Baker31acc652016-06-23 15:47:56 -0700243 return SFlowTenant.get_tenant_objects_by_user(request.user)
244
rdudyala996d70b2016-10-13 17:40:55 +0000245class OpenStackServiceMonitoringPublisherForm(forms.ModelForm):
246 creator = forms.ModelChoiceField(queryset=User.objects.all())
247
248 def __init__(self,*args,**kwargs):
249 super (OpenStackServiceMonitoringPublisherForm,self ).__init__(*args,**kwargs)
250 self.fields['kind'].widget.attrs['readonly'] = True
251 self.fields['provider_service'].queryset = CeilometerService.get_service_objects().all()
252 if self.instance:
253 # fields for the attributes
254 self.fields['creator'].initial = self.instance.creator
255 if (not self.instance) or (not self.instance.pk):
256 # default fields for an 'add' form
257 self.fields['kind'].initial = CEILOMETER_PUBLISH_TENANT_OS_KIND
258 self.fields['creator'].initial = get_request().user
259 if CeilometerService.get_service_objects().exists():
260 self.fields["provider_service"].initial = CeilometerService.get_service_objects().all()[0]
261
262 def save(self, commit=True):
263 self.instance.creator = self.cleaned_data.get("creator")
264 return super(OpenStackServiceMonitoringPublisherForm, self).save(commit=commit)
265
266 class Meta:
267 model = OpenStackServiceMonitoringPublisher
268 fields = '__all__'
269
270class OpenStackServiceMonitoringPublisherAdmin(ReadOnlyAwareAdmin):
271 list_display = ('backend_status_icon', 'id', )
272 list_display_links = ('backend_status_icon', 'id')
273 fieldsets = [ (None, {'fields': ['backend_status_text', 'kind', 'provider_service', 'service_specific_attribute', 'creator'],
274 'classes':['suit-tab suit-tab-general']})]
275 readonly_fields = ('backend_status_text', 'service_specific_attribute' )
276 form = OpenStackServiceMonitoringPublisherForm
277
278 suit_form_tabs = (('general','Details'),)
279 actions=['delete_selected_objects']
280
281 def get_actions(self, request):
282 actions = super(OpenStackServiceMonitoringPublisherAdmin, self).get_actions(request)
283 if 'delete_selected' in actions:
284 del actions['delete_selected']
285 return actions
286
287 def delete_selected_objects(self, request, queryset):
288 for obj in queryset:
289 obj.delete()
290 delete_selected_objects.short_description = "Delete Selected OpenStackServiceMonitoringPublisher Objects"
291
292 def get_queryset(self, request):
293 return OpenStackServiceMonitoringPublisher.get_tenant_objects_by_user(request.user)
294
295class ONOSServiceMonitoringPublisherForm(forms.ModelForm):
296 creator = forms.ModelChoiceField(queryset=User.objects.all())
297 onos_service_endpoints = forms.CharField(max_length=1024, help_text="IP addresses of all the ONOS services to be monitored")
298
299 def __init__(self,*args,**kwargs):
300 super (ONOSServiceMonitoringPublisherForm,self ).__init__(*args,**kwargs)
301 self.fields['kind'].widget.attrs['readonly'] = True
302 self.fields['provider_service'].queryset = CeilometerService.get_service_objects().all()
303 if self.instance:
304 # fields for the attributes
305 self.fields['creator'].initial = self.instance.creator
306 self.fields['onos_service_endpoints'].initial = self.instance.onos_service_endpoints
307 if (not self.instance) or (not self.instance.pk):
308 # default fields for an 'add' form
309 self.fields['kind'].initial = CEILOMETER_PUBLISH_TENANT_ONOS_KIND
310 self.fields['creator'].initial = get_request().user
311 if CeilometerService.get_service_objects().exists():
312 self.fields["provider_service"].initial = CeilometerService.get_service_objects().all()[0]
313
314 def save(self, commit=True):
315 self.instance.creator = self.cleaned_data.get("creator")
316 self.instance.onos_service_endpoints = self.cleaned_data.get("onos_service_endpoints")
317 return super(ONOSServiceMonitoringPublisherForm, self).save(commit=commit)
318
319 class Meta:
320 model = ONOSServiceMonitoringPublisher
321 fields = '__all__'
322
323class ONOSServiceMonitoringPublisherAdmin(ReadOnlyAwareAdmin):
324 list_display = ('backend_status_icon', 'id', )
325 list_display_links = ('backend_status_icon', 'id')
326 fieldsets = [ (None, {'fields': ['backend_status_text', 'kind', 'provider_service', 'service_specific_attribute', 'creator', 'onos_service_endpoints'],
327 'classes':['suit-tab suit-tab-general']})]
328 readonly_fields = ('backend_status_text', 'service_specific_attribute' )
329 form = ONOSServiceMonitoringPublisherForm
330
331 suit_form_tabs = (('general','Details'),)
332 actions=['delete_selected_objects']
333
334 def get_actions(self, request):
335 actions = super(ONOSServiceMonitoringPublisherAdmin, self).get_actions(request)
336 if 'delete_selected' in actions:
337 del actions['delete_selected']
338 return actions
339
340 def delete_selected_objects(self, request, queryset):
341 for obj in queryset:
342 obj.delete()
343 delete_selected_objects.short_description = "Delete Selected OpenStackServiceMonitoringPublisher Objects"
344
345 def get_queryset(self, request):
346 return ONOSServiceMonitoringPublisher.get_tenant_objects_by_user(request.user)
347
348class UserServiceMonitoringPublisherForm(forms.ModelForm):
349 creator = forms.ModelChoiceField(queryset=User.objects.all())
350 exclude_service_list = ['ceilometer', 'onos', 'VTN', 'vROUTER', 'vOLT', 'vTR']
351 target_service = forms.ModelChoiceField(queryset=Service.objects.all().exclude(kind__in=exclude_service_list))
352
353 def __init__(self,*args,**kwargs):
354 super (UserServiceMonitoringPublisherForm,self ).__init__(*args,**kwargs)
355 self.fields['kind'].widget.attrs['readonly'] = True
356 self.fields['provider_service'].queryset = CeilometerService.get_service_objects().all()
357 if self.instance:
358 # fields for the attributes
359 self.fields['creator'].initial = self.instance.creator
360 self.fields['target_service'].initial = self.instance.target_service
361 if (not self.instance) or (not self.instance.pk):
362 # default fields for an 'add' form
363 self.fields['kind'].initial = CEILOMETER_PUBLISH_TENANT_USER_KIND
364 self.fields['creator'].initial = get_request().user
365 if CeilometerService.get_service_objects().exists():
366 self.fields["provider_service"].initial = CeilometerService.get_service_objects().all()[0]
367
368 def save(self, commit=True):
369 self.instance.creator = self.cleaned_data.get("creator")
370 self.instance.target_service = self.cleaned_data.get("target_service")
371 return super(UserServiceMonitoringPublisherForm, self).save(commit=commit)
372
373 class Meta:
374 model = UserServiceMonitoringPublisher
375 fields = '__all__'
376
377class UserServiceMonitoringPublisherAdmin(ReadOnlyAwareAdmin):
378 list_display = ('backend_status_icon', 'id', )
379 list_display_links = ('backend_status_icon', 'id')
380 fieldsets = [ (None, {'fields': ['backend_status_text', 'kind', 'provider_service', 'service_specific_attribute', 'creator', 'target_service'],
381 'classes':['suit-tab suit-tab-general']})]
382 readonly_fields = ('backend_status_text', 'service_specific_attribute' )
383 form = UserServiceMonitoringPublisherForm
384
385 suit_form_tabs = (('general','Details'),)
386 actions=['delete_selected_objects']
387
388 def get_actions(self, request):
389 actions = super(UserServiceMonitoringPublisherAdmin, self).get_actions(request)
390 if 'delete_selected' in actions:
391 del actions['delete_selected']
392 return actions
393
394 def delete_selected_objects(self, request, queryset):
395 for obj in queryset:
396 obj.delete()
397 delete_selected_objects.short_description = "Delete Selected UserServiceMonitoringPublisher Objects"
398
399 def get_queryset(self, request):
400 return UserServiceMonitoringPublisher.get_tenant_objects_by_user(request.user)
401
402class InfraMonitoringAgentInfoForm(forms.ModelForm):
403 class Meta:
404 model = InfraMonitoringAgentInfo
405 widgets = {
406 'start_url_json_data': UploadTextareaWidget(attrs={'rows': 5, 'cols': 80, 'class': "input-xxlarge"}),
407 }
408 fields = '__all__'
409
410class InfraMonitoringAgentInfoAdmin(XOSBaseAdmin):
411 list_display = ('backend_status_icon', 'name', 'id', )
412 list_display_links = ('backend_status_icon', 'name', 'id')
413 fieldsets = [ (None, {'fields': ['name', 'start_url', 'start_url_json_data', 'stop_url', 'monitoring_publisher'],
414 'classes':['suit-tab suit-tab-general']})]
415 form = InfraMonitoringAgentInfoForm
416
417 suit_form_tabs = (('general','Details'),)
418
419class MonitoringCollectorPluginInfoForm(forms.ModelForm):
420 class Meta:
421 model = MonitoringCollectorPluginInfo
422 #widgets = {
423 # 'plugin_notification_handlers_json': UploadTextareaWidget(attrs={'rows': 5, 'cols': 80, 'class': "input-xxlarge"}),
424 #}
425 fields = '__all__'
426
427class MonitoringCollectorPluginInfoAdmin(XOSBaseAdmin):
428 list_display = ('backend_status_icon', 'name', 'id', )
429 list_display_links = ('backend_status_icon', 'name', 'id')
430 fieldsets = [ (None, {'fields': ['name', 'plugin_folder_path', 'plugin_rabbit_exchange', 'monitoring_publisher'],
431 'classes':['suit-tab suit-tab-general']})]
432 form = MonitoringCollectorPluginInfoForm
433
434 suit_form_tabs = (('general','Details'),)
435
Scott Baker31acc652016-06-23 15:47:56 -0700436admin.site.register(CeilometerService, CeilometerServiceAdmin)
437admin.site.register(SFlowService, SFlowServiceAdmin)
438admin.site.register(MonitoringChannel, MonitoringChannelAdmin)
439admin.site.register(SFlowTenant, SFlowTenantAdmin)
rdudyala996d70b2016-10-13 17:40:55 +0000440admin.site.register(OpenStackServiceMonitoringPublisher, OpenStackServiceMonitoringPublisherAdmin)
441admin.site.register(ONOSServiceMonitoringPublisher, ONOSServiceMonitoringPublisherAdmin)
442admin.site.register(UserServiceMonitoringPublisher, UserServiceMonitoringPublisherAdmin)
443admin.site.register(InfraMonitoringAgentInfo, InfraMonitoringAgentInfoAdmin)
444admin.site.register(MonitoringCollectorPluginInfo, MonitoringCollectorPluginInfoAdmin)
Scott Baker31acc652016-06-23 15:47:56 -0700445