Learn how to a build a cloud-first strategyRegister Now

x
  • Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 2318
  • Last Modified:

VBScript Required. Read information from Excel Sreadsheet/csv file and add users to security groups and security groups to security groups

Hello All,

I am in need of a VBScript to manage AD Users and Security Groups.

What I am trying to achieve:
According to the attached .xls spreadsheets you will see in template.xls there are AD User accounts listed against Security Groups and in Template2.xls there are security groups listed against their higher level security groups.

I need a VBS that can read the columns in these .xls or .csv and then perform the folllwing;

Template.xls (.csv whichever format suits to reading info effectively)
Add Users in the users column to their corresponding security groups

Template2.xls (.csv whichever format suits to reading info effectively)
Add Security Groups listed under Level1 Group column to level2 Security Group.

there will be an empty row after a security group listing finishes. so there could be some check done for an empty row or something like that.

I am sure you guys will need more info that what i gave you above please let me know what other info you need.

Thanks in advance.

VM


Template.xls
Template2.xls
0
vithal_m
Asked:
vithal_m
  • 12
  • 9
  • 3
  • +1
5 Solutions
 
dan_nealCommented:
Please check out sample scripts in the Active Directory section of the Microsoft Scripting Guys website.
http://www.microsoft.com/technet/scriptcenter/hubs/default.mspx 
0
 
vithal_mAuthor Commented:
Hello Dan,

Yes I am going through the script center. My problem is with reading from .xls or a .csv, knowing where to stop and where to start..

Thanks
VM
0
 
Mark PavlakCommented:
Here is a script that I worte to import users from a csv which was created via a spreadsheet to import mass amounts of people. This creates a Home Folder, Mailbox on apporaite store, adds to specified security groups and documents user account consistent with the policies of my network. You can probally piece out the functionality you are looking for. I have also attached the Spread sheet.
'==========================================================================
'
' VBScript Source File -- Created with SAPIEN Technologies PrimalScript 2007
'
' NAME: Import_Users_From_SpreadSheet.vbs
'
' AUTHOR:  
' DATE  : 3/26/2009
'
' COMMENT: Imports users from pre-defined excel spreadsheet, CSV must use PIPE |  inorder to parse file
'
'==========================================================================
'Globals	
'=============================================================================================
Dim objFso,objInfile,objOutfile
Dim strAD_FirstName,strAD_LastName,strAD_Description,strAD_Office,strAD_Company,strAD_Phone, _
   strAD_SAMAccountName,strAD_Title,strAD_DisplayName,strFileName,strAD_cn,strAD_Hdrive,strAD_MemberOf,_
   strAD_HomeMDB,strAD_mailnickname,strAD_Password,strUserCN,strAD_UserPrincipleName,strAD_telephoneNumber,_
   strAD_HomeDrive,strAD_HomePhone,strAD_TerminalServicesHomeDirectory,strAD_protocolSettings
Const ForReading = 1
Dim ArrUser,ArrSec_Groups,ArrDist_Groups
'Arr User Layout
'|        0      | 1 |   2   |    3     |     4    |       5       |   6   |     7      |  8    |   9    |  10  | 
'| Employee Type |OU |Branch |FirstName | LastName |SAMAccountName | Title |Description |Office |Company |Phone | 
'****************************************************************************************************************
' |   11     |           12          |            13         |
' |H Drive  |Eschange Storage Group |Exchange Mailbox Store |
'=============================================================================================
'Initialize Globals
'=============================================================================================
Set objFso = CreateObject("Scripting.FileSystemObject")
Set objOutfile = objFso.CreateTextFile("c:\ImportResults.txt")
strFileName = BrowseForFile("C:\")
Set objInfile = objFso.OpenTextFile(strFileName,ForReading)
strAD_FirstName = "givenName"
strAD_LastName = "sn"
strAD_Description = "description"
strAD_Office = "physicalDeliveryOfficeName"
strAD_Company = "company"
strAD_Phone = "telephoneNumber"
strAD_cn = "cn"
strAD_SAMAccountName = "sAMAccountName"
strAD_Title = "title"
strAD_DisplayName = "name"
strAD_Hdrive ="homeDirectory"
strAD_MemberOf = "memberOf"
strAD_HomeMDB = "homeMDB"
strAD_mailnickname = "mailNickname"
strAD_Password = "bofhr@GWFH"
strAD_UserPrincipleName = "userPrincipalName"
strAD_telephoneNumber = "telephoneNumber"
strAD_HomeDrive = "homeDrive"
strAD_HomePhone = "homePhone"
strAD_TerminalServicesHomeDirectory = "TerminalServicesHomeDirectory"
strAD_protocolSettings = "protocolSettings"
 
'=============================================================================================
' strFirstName StrLastName strDescription strOffice strCompany strPhone strMobile strIPPhone strSAMAccountName  
' strTitle strDisplayName
'Load CSV
'=============================================================================================
objInfile.ReadLine 'throw out headers
'=============================================================================================
 
Do Until objInfile.AtEndOfStream
ArrUser = Split(objInfile.readline,"|")
strUserCN = Create_User_CN(ArrUser)
objOutfile.WriteLine strUserCN 
Initialize_Group_Arrays (ArrUser(0))
Initialize_Distrubution_Groups_Array (ArrUser(9))
Create_User ArrUser(0),ArrUser(1),ArrUser(2),ArrUser(3),ArrUser(4),ArrUser(5),ArrUser(6),ArrUser(7), _
			ArrUser (8), ArrUser(9),ArrUser(10)	
Add_User_To_Standard_Groups ArrSec_Groups,strUserCN	
Add_Email_Distrubution_Groups ArrDist_Groups,strUserCN
Create_H_Drive ArrUser(3)&ArrUser(4),ArrUser(2)			
Loop	
 
		
objOutfile.Close
Sub Create_User (strEmployeeType,strOU,strBranch,strFirstName,strLastName,strSAMAccountName,strTitle,strDescription, _
				 strOffice,strCompany,strPhone)
'Variables
'=============================================================================================
Dim objRoot,ObjDomain,objOU,objUser
'=============================================================================================
'Bind to AD
'=============================================================================================
Set objRoot = GetObject("LDAP://rootDSE")
ObjDomain = objRoot.Get("defaultNamingContext")
'WScript.Echo strOU&ObjDomain
Set objOU = GetObject("LDAP://" & strOU&ObjDomain)
'=============================================================================================
'WScript.Echo ObjDomain
Set objUser = objOU.Create("User", "cn=" & strFirstName & " " & strLastName)
'WScript.Echo strPhone
With objUser
	.Put strAD_FirstName,strFirstName
	.Put strAD_LastName,strLastName
	.Put strAD_SAMAccountName,strSAMAccountName
	.Put strAD_Title,strTitle
	.Put strAD_Description,strDescription
	.Put strAD_Office,strOffice
	.Put strAD_Company,strCompany
	.Put strAD_Phone,strPhone
	.Put strAD_telephoneNumber, strPhone
	.Put strAD_UserPrincipleName, strSAMAccountName & "@yourdomainhere"
	.Put strAD_Hdrive,"\\"&strBranch&"server01\users$\"&strFirstName&strLastName
	.put strAD_HomeDrive , "H:"
	.put strAD_HomePhone , strPhone
	.SetInfo
End With
With objUser 
	.put strAD_HomeMDB , Build_Exchange_homeMDB(strEmployeeType)
	.put strAD_mailnickname , strSAMAccountName
	.SetInfo
End With
objUser.TerminalServicesHomeDirectory = "E:\Documents and Settings\"&strFirstName&strLastName
objUser.PutEx 2, "protocolSettings",ARRAY("HTTP§0§1§§§§§§","IMAP4§0§1§4§ISO-8859-1§0§1§0§0","POP3§0§1§4§ISO-8859-1§0§§§") 
objUser.msExchOmaAdminWirelessEnable = 7
objUser.userAccountControl = 512
objUser.SetPassword strAD_Password
objUser.SetInfo
'wscript.Echo Build_Exchange_homeMDB(strEmployeeType)
End Sub
 
Sub Create_H_Drive (strUser,strBranch)
'Variables
'=============================================================================================
Dim objNetwork,objShell
Dim strCmd
'=============================================================================================
'initalize variables
'=============================================================================================
Set objNetwork = CreateObject ("WScript.Network")
Set objShell = CreateObject ("WScript.Shell")
strCmd = "%ComSpec% /c Echo Y| cacls b:\testuser /t /c  /g "&chr(34)&_
		 "yourdomainhere\domain admins"&chr(34)&":F /p "&chr(34)&_
		 "yourdomainhere\"&strUser&chr(34)&":F"
'=============================================================================================
objNetwork.MapNetworkDrive "b:","\\"&strBranch&"server01\users$"
'check for folder on server if it doesnt exist it will create
'=============================================================================================
If objFso.FolderExists("b:\"&strUser) Then
objFso.DeleteFolder ("b:\"&strUser)
Else
objFso.CreateFolder "b:\"&strUser
End If 
'=============================================================================================
objShell.Run strCmd,2,True
objNetwork.RemoveNetworkDrive "b:",True
End Sub
Sub ScreenPrintArray (arrTemp)
'This Prints out any arr elements
'Variables
'=============================================================================================
Dim intArraySize,i
'=============================================================================================
'Initialize Variables
'=============================================================================================
i=0
intArraySize = UBound (arrTemp)
'=============================================================================================
'WScript.Echo "There are "&intArraySize&" elements in the array"
Do While i <> intArraySize
'	WScript.Echo "Element "&i &"is : "&arrTemp(i)
	i = i+1
Loop
End Sub
Sub drivemapper (drive,share)
'Map Network drive
Dim network,drives,i
Set network = WScript.CreateObject("WScript.Network")
Set drives = network.EnumNetworkDrives
For i=0 to drives.count -1 Step 2
If LCase(drive)= LCase(drives.item(i)) Then
network.RemoveNetworkDrive drive, True,True 
End If
Next
network.MapNetworkDrive drive, share
End Sub
Sub Initialize_Group_Arrays (strEmployeeType)
Dim strTemp
 

ArrSec_Groups = Split (strTemp,"|")
End Sub
Sub Initialize_Distrubution_Groups_Array (strCompany)
Dim strTemp
If strCompany = "Your company here" Then
	strTemp = "You DN to Groups here"
End If 
ArrDist_Groups = Split (strTemp,"|")
End Sub
Sub Add_User_To_Standard_Groups (arrTemp,strTempCN)
'Variables
'=============================================================================================
Dim objGroup
Dim intMode,intControl
'=============================================================================================
intControl = UBound(arrTemp)
intMode = 0
Do Until intControl = 0
Set objGroup = GetObject("LDAP://"&arrTemp(intMode))
objGroup.Add "LDAP://"&strTempCN
intControl = intControl - 1
intMode = intMode + 1
set objGroup = Nothing
Loop
End Sub
Sub Add_Email_Distrubution_Groups (arrTemp,strTempCN)
'Variables
'=============================================================================================
Dim objGroup
Dim intMode,intControl
'=============================================================================================
intControl = UBound(arrTemp)
intMode = 0
Do Until intControl = 0
Set objGroup = GetObject("LDAP://"&arrTemp(intMode))
objGroup.Add "LDAP://"&strTempCN
intControl = intControl - 1
intMode = intMode + 1
set objGroup = Nothing
Loop
End sub
Function BrowseForFile (path)
Dim objDialog
Set objDialog = CreateObject("UserAccounts.CommonDialog")
With objDialog
	.Filter = " CSV|*.csv"
	.Flags = &H80000
	.FilterIndex = 1
	.InitialDir = path
End With
intResult = objDialog.ShowOpen
If intResult = 0 Then
    Wscript.Quit
Else
    BrowseForFile = objDialog.FileName
End If
End Function
Function Create_User_CN (arrTemp)
'Get Domain Name
Set objRoot = GetObject("LDAP://rootDSE")
ObjDomain = objRoot.Get("defaultNamingContext")
Create_User_CN = "CN=" + arrTemp(3) + " " + arrTemp(4) + "," + arrTemp(1) + ObjDomain
End Function
Function Build_Exchange_homeMDB (strTemp)
'Variables
'=============================================================================================
Dim strServer,strExchangeorg,strDomain,strAdminGroup,strStorageGroup,strStoreName,strMailBox
'=============================================================================================
'Initialize Variables
'=============================================================================================
strServer = "MAILSERVERNAMEHERE"
strExchangeorg = "YourOrgHere"
strDomain = "DC=yourdomainhere"
'=============================================================================================
'Determine Mailbox Store and Storage Group
'=============================================================================================

'=============================================================================================
'Build MDB
'=============================================================================================
strMailbox = strMailbox & "CN=" & strStoreName
strMailbox = strMailbox & ",CN=" & strStorageGroup
strMailbox = strMailbox & ",CN=InformationStore"
strMailbox = strMailbox & ",CN=" & strServer
strMailbox = strMailbox & ",CN=Servers"
strMailbox = strMailbox & ",CN=" & strAdminGroup
strMailbox = strMailbox & ",CN=Administrative Groups"
strMailbox = strMailbox & ",CN=" + strExchangeOrg
strMailbox = strMailbox & ",CN=Microsoft Exchange"
strMailbox = strMailbox & ",CN=Services"
strMailbox = strMailbox & ",CN=Configuration"
strMailbox = strMailbox & "," & strDomain
'=============================================================================================
Build_Exchange_homeMDB =  strMailBox
End Function

Open in new window

User-Import--version-3-.xls
0
What does it mean to be "Always On"?

Is your cloud always on? With an Always On cloud you won't have to worry about downtime for maintenance or software application code updates, ensuring that your bottom line isn't affected.

 
vithal_mAuthor Commented:
jfinner2, thanks for the reply.

I will go through the code and see if i can use any parts of it..the problem I will have is how do I tell the script to stop after reading one line and then continue once you are done with the commands.

As shown in my excel template...

Column 1 will have the higher level security group.  So the script has to read the group name in Column 1 and then add all the users given in Column 2 once done it will then proceed and check for next. I will neep to put some kind of check in Column 1 for the scritpt to read and stop. once that group is done it will then read the next group and add all users to that and so on..

Did I make any sense?

0
 
vithal_mAuthor Commented:
how about if I have a CSV file created in this format
"%SecurityGroup1%","%username%,%username%,%username%, ....so on
"%SecurityGroup2%","%username%,%username%,%username%, ....so on
"%SecurityGroup3%","%username%,%username%,%username%, ....so on
"%SecurityGroup4%","%username%,%username%,%username%, ....so on

would it be easy to store the text before the first comma into array(0) and then all the others following that into usernamearray(1), usernamearray(2) etc..at the end of line move to the next line etc..

Thanks
VM
0
 
Mark PavlakCommented:
Sorry I have been sick,  That would work if you used the csv with %Sec1%,u1,u2,u3etc

You would want to do a couple of things though.  In your regional settings on your PC change your delimiter from , to | .  The reason I say this is that AD uses "," ie CN=johnfinner,OU=Myusers,DC=whaterver,DC=com   Inorder to use the CSV and Arrays the EASIEST thing I think is to go ahead and read inl the line and use the split function to create the array knowing that index 0 is the Security group.  It would look something like this

Dim oFso,oInfile
Dim  aTemp
Const FroReading = 1
set oFso = Createobject("Scripting.FileSysytemObject")
set oInfile = oFso.openTextFile("c:\Filename.csv",ForReading)
Do until oInfile.AtEndofStream
      aTemp = split(oInfile.readline,"|")
  Process_Security_Group(aTemp)
loop

Sub Process_Security_Group(aGroup)
    'connect to AD
    'Retrieve security group Identified on aGroup(0)
    ' Use Ubound to find the amount of elements in aGroup start in an offset of 1 and loop through to add members
end sub


I hope this helps if yo uneed some help with the sub developemnt let me know
0
 
vithal_mAuthor Commented:
thanks heaps for the response.
i will give it a go and let you know if i need help. i've never used ubound before but I think it should be ok at this stage. i will do some research on that.

Also I should request the .csv file to be made in the following format
%secgroup%|u1|u2|u3|u4|
etc..

Thanks,
VM
0
 
Mark PavlakCommented:
Since you are using split to define the array you need ubound to return the upper limits of the array to determine its size.  IE
dim sString,aArray

sString = "1,2,3,4,5"
aArray = split(aString,",")
wscript.echo uBound(aArray)    will return 4  the reason being that arrays start @ 0
0
 
bryan oakley-wigginsSenior Cloud EngineerCommented:
Hi
If you want to have 1 column with the security group name and 1 column with user(s), then you can use powershell and Quest AD cmdlets:

Create a .csv file with ColumnA (security group) and ColumnB (Users [seperate with a semi-colon ; for multiple users]

(I got this code from Chris-Dent)

ForEach ($Entry in (Import-CSV "c:\scripts\test_data\imp1.csv")) {
  ($Entry.ColumnB).Split(";") | %{ Add-QADGroupMember $Entry.ColumnA -Member $_ }
}

Worked a treat for me..!

Cheers
Bry
0
 
vithal_mAuthor Commented:
Hi Bry,

Thanks heaps for the reply. Sounds promising...but I am new to PowerShell could you please tell me where to start...

I have installed PowerShell on my PC.

Thanks,
VM
0
 
bryan oakley-wigginsSenior Cloud EngineerCommented:
Hi vithal m

I am fairly new to powershell as well, so will try my best to assist you :-)
I had a situation where I needed to bulk add users to a bunch of security groups from a .csv file and was provided a solution from an EE expert (Chris-Dent).

Attached is an example .jpg of a csv file [apologies, EE wouldn't let me upload the .csv..!] (name it Groupimport.csv - or a name that suits you).

I then downloaded and installed Quest's 'free' ad cmdlets (a must have..!) to extend my powershell install (currently using the version 1.0, so you should be fine)
http://www.quest.com/powershell/activeroles-server.aspx

Literally copy this code into a file and name it: Useradd2Groups.PS1 (or  a name that suits you).
You can then either double-click the file or drag into a powershell shell window (I generally use the Quest shell window) so you can monitor output.

***I got this code from Chris-Dent who provided this solution for my request***

ForEach ($Entry in (Import-CSV "c:\scripts\test_data\GroupImport.csv")) {
  ($Entry.ColumnB).Split(";") | %{ Add-QADGroupMember $Entry.ColumnA -Member $_ }
}

et viola - Your users will be added to the relevant groups (so long as they exist).
do remember, for multiple users to separate each user with a semi-colon ; - Any errors will be dumped on the shell window in red...

I can look at the group nesting a bit later at work if I can, but it shouldn't be too much hassle.
Any probs, shout me back.

Cheers
Bry

GroupImportCSV.jpg
0
 
bryan oakley-wigginsSenior Cloud EngineerCommented:
before you actually 'run' the script open a powershell session and do the following:

import-csv "yourInputfile.csv"

***you should get back a list of the groups and users in their associated columns***
0
 
vithal_mAuthor Commented:
Hi BryanOakley!
Thanks heaps for your response. I am giving it a try now.

Will let you know if I am stuck somewhere.

Thanks,
Vithal
0
 
bryan oakley-wigginsSenior Cloud EngineerCommented:
how did you get on?
0
 
vithal_mAuthor Commented:
Ah...

Experts-Exchange was down yesterday..none of our team mates could get to it...i lost the message i typed.....anyway

It worked.....and I have actually started learning PowerShell....it is great...

also the same code works for adding security groups to securityy groups as well...

but one thing i found was error trapping....i am not sure how to get that done....

when I am adding hundreds of users to SGs etc....i will need some kind or error reporting to a .txt file saying...

error this user could not be added to the groupor something in those lines...

is it possible at all?

Thanks heaps...

Vithal
0
 
bryan oakley-wigginsSenior Cloud EngineerCommented:
Hi vithal

Good to hear it is working and good call on the groups to groups (I hadn't tested that bit :-)

As for error log - the shell actually presents any errors it encounters, so you could either | out-file c:\scripts\errors\MYERROR.log or by > c:\etc\etc.log ???
And this way you would get a list of any errors encountered...

I hope this is what you were after...

Cheers
Bry
0
 
bryan oakley-wigginsSenior Cloud EngineerCommented:
hi vithal

how did you get on?

cheers
Bry
0
 
vithal_mAuthor Commented:
Hi Bry

I just got back from leave today......will try it now and let you know..

Thanks
Vithal
0
 
vithal_mAuthor Commented:
Hi Bryan

I've used the following command to output the log..when i check the log it comes up as shown below..

ForEach ($Entry in (Import-CSV "c:\Import.csv")) {
  ($Entry.ColumnB).Split(";") | %{ Add-QADGroupMember $Entry.ColumnA -Member $_ } | out-file c:\NVIDIA\test.log -Verbose
}

Vithal Maddala                 user            CN=Vithal Maddala,OU=CFSG Users,OU=Challenger Use...

I dont know what to do to get the full details and not ...... at the end..
 is -detailed an option?

what if I want to say simply if group addition successful report "Success" if an error occured report "failure" ?

cheers
Vithal
0
 
bryan oakley-wigginsSenior Cloud EngineerCommented:
hi Vithal

had a quick check and the normal out-file doesn't seem to work as wanted on this occasion..!
You can normally out-file with no probs but I believe with these actions we would have to input error checking on each action...

Pretty much (as far as I understand) you only get back errors so we could run through the script and then we can set a screen capture to then output all the shell buffer output to a file for error checking? (a bit clumsy I know) but still works...

I.e -
save the below code to get-consoleastext.ps1
Run your .\script.ps1 in the shell
then run .\get-consoleastext.ps1 | out-file c:\PATH\FILE.log (you will then get a screen-grab of any errors/issues encountered which PoSH outputs to the buffer) - otherwise, you can assume all your actions succeeded...

Hope this helps..!
Cheers
Bry


#################################################################################################################
# Get-ConsoleAsText.ps1
#
# The script captures console screen buffer up to the current cursor position and returns it in plain text format.
#
# Returns: ASCII-encoded string.
#
# Example:
#
# $textFileName = "$env:c:\temp\ConsoleBuffer.txt"
# .\Get-ConsoleAsText | out-file $textFileName -encoding ascii
# $null = [System.Diagnostics.Process]::Start("$textFileName")
#

# Check the host name and exit if the host is not the Windows PowerShell console host.
if ($host.Name -ne 'ConsoleHost')
{
  write-host -ForegroundColor Red "This script runs only in the console host. You cannot run this script in $($host.Name)."
  exit -1
}

# Initialize string builder.
$textBuilder = new-object system.text.stringbuilder

# Grab the console screen buffer contents using the Host console API.
$bufferWidth = $host.ui.rawui.BufferSize.Width
$bufferHeight = $host.ui.rawui.CursorPosition.Y
$rec = new-object System.Management.Automation.Host.Rectangle 0,0,($bufferWidth - 1),$bufferHeight
$buffer = $host.ui.rawui.GetBufferContents($rec)

# Iterate through the lines in the console buffer.
for($i = 0; $i -lt $bufferHeight; $i++)
{
  for($j = 0; $j -lt $bufferWidth; $j++)
  {
    $cell = $buffer[$i,$j]
    $null = $textBuilder.Append($cell.Character)
  }
  $null = $textBuilder.Append("`r`n")
}

return $textBuilder.ToString()
0
 
vithal_mAuthor Commented:
Hi Bryan,

Thanks for the reply..I will give it a try and let you know.

Cheers
Vithal
0
 
bryan oakley-wigginsSenior Cloud EngineerCommented:
hi Vithal

Any joy?
I am currently looking at writing out errors, success etc with:
write-host "something";write-output "something" | out-file -path c:\logs\log.log -append

I will have a play at putting this in the group code and see what we can come up with..!
I do hope further versions of powershell make it easier to error trap etc with a simple pipe out to file (although I believe PoSH stdout stderror functions are somehwhat a bit different to other shells)

Anyways, half the fun is the journey right? :-)

Cheers
Bry
0
 
vithal_mAuthor Commented:
Hey Bryan,

I was away on leave again for a aweek. Just came back today.
I tried the script. It got stuck in a loop after I ran the console capture script. But anyhow we managed to do the job with the script without the error logging. Thanks for all your help.

I will love to keep this post open but I am also happy to close it by assigning you points..do you want me to open one more question and discuss in that?

Cheers,
Vithal
0
 
bryan oakley-wigginsSenior Cloud EngineerCommented:
Hi Vithal

I am so glad that you have completed the job and I am glad that I may have (in some little way) helped pass on some of the solutions I have been helped with.
Points is always your call :-) but for a better response you could open another question and put that Q under the powershell zone and you will get results asap I am sure.

All the best
Bry
0
 
vithal_mAuthor Commented:
A simple solution for a what could have a laborious task if done manually. Thanks to jfinner2 for initial input and thanks heaps to Bryan for guiding me to the shores.  I have actually a great deal about PowerShell after Bryan introduced me to it in this post. Your keenness in finding out what was happening with my task made me to try it even harder to complete the task. Thanks again Bryan!
0

Featured Post

Hire Technology Freelancers with Gigs

Work with freelancers specializing in everything from database administration to programming, who have proven themselves as experts in their field. Hire the best, collaborate easily, pay securely, and get projects done right.

  • 12
  • 9
  • 3
  • +1
Tackle projects and never again get stuck behind a technical roadblock.
Join Now