February 2009 - Posts

Module Manifests

When we looked at modules we used Export-Module at the end of the module file to control which functions were made visible.  This becomes a little awkward if we have a large number of functions that we may want to use in differing combinations.

The solution is to use a module manifest.  This is a file that looks something like this

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
#
# Module manifest for module 'user'
#
# Generated by: Richard Siddaway
#
# Generated on: 22/02/2009
#

@{
# These modules will be processed when the module manifest is loaded.
NestedModules = 'user.psm1'

# This GUID is used to uniquely identify this module.
GUID = 'b55021a4-5a21-4cf6-9b76-29eef95db0cf'

# The author of this module.
Author = 'Richard Siddaway'

# The company or vendor for this module.
CompanyName = 'Macdui'

# The copyright statement for this module.
Copyright = '(c) Richard Siddaway'

# The version of this module.
ModuleVersion = '1.0'

# A description of this module.
Description = 'Module of scripts for working with AD user accounts'

# The minimum version of PowerShell needed to use this module.
PowerShellVersion = '2.0'

# The CLR version required to use this module.
CLRVersion = '2.0'

# Functions to export from this manifest.
ExportedFunctions = 'new-password'

# Aliases to export from this manifest.
ExportedAliases = '*'

# Variables to export from this manifest.
ExportedVariables = '*'

# Cmdlets to export from this manifest.
ExportedCmdlets = '*'

# This is a list of other modules that must be loaded before this module.
RequiredModules = @()

# The script files (.ps1) that are loaded before this module.
ScriptsToProcess = @()

# The type files (.ps1xml) loaded by this module.
TypesToProcess = @()

# The format files (.ps1xml) loaded by this module.
FormatsToProcess = @()

# A list of assemblies that must be loaded before this module can work.
RequiredAssemblies = @()

# Lists additional items like icons, etc. that the module will use.
OtherItems = @()

# Module specific private data can be passed via this member.
PrivateData = ''

}

I generated this one on Windows 7.  Use New-Module manifest without any parameters and you will be prompted for the required information.  The rest can be filled in using your favourite editor.  The CTP3 version uses slightly different names in some places so the two aren’t completely compatible.

The manifest can be tested

PS> Invoke-History 59
Test-ModuleManifest ./user2.psd1

Name              : user2.psd1
Path              : C:\Scripts\wip\user2.psd1
Description       : Module of scripts for working with AD user accounts
Guid              : b55021a4-5a21-4cf6-9b76-29eef95db0cf
Version           : 1.0
ModuleBase        : C:\Scripts\wip
ModuleType        : Manifest
PrivateData       :
AccessMode        : ReadWrite
ExportedAliases   : {}
ExportedCmdlets   : {}
ExportedFunctions : {}
ExportedVariables : {}
NestedModules     : {}

 

Notice the use of invoke-history to rerun a previous command.

That is the basics of modules covered.  Next task is to add some functions and modify the manifest so that I can use it to create users

 

Technorati Tags: ,
Posted by Richard Siddaway's Blog
Filed under:

LiveMeeting – Last Call

7pm GMT on Thursday February 26th sees Rolf Masuch presenting on using PowerShell as Active Directory Login Script.  Details here http://richardsiddaway.spaces.live.com/default.aspx?_c01_BlogPart=blogentry&_c=BlogPart&handle=cns!43CFA46A74CF3E96!2079

 

Technorati Tags: ,

Windows 7

Just installed Windows 7 on an old laptop – 0.75GB RAM single core.  It works a treat and is reasonably responsive.  It is more than enough for Office type applications and Internet.  Well impressed as Vista wouldn’t even look at this machine.

Looks like I’ve just extended the life time of that machine.

Few issues with drivers that I’m still resolving but nothing thats a show stopper.

Best of all it has PowerShell v2 installed.

 

Technorati Tags:
Posted by Richard Siddaway's Blog
Filed under:

PowerShell UG March

The March meeting will be on Thursday 26th March at 6.30pm

Location:  Microsoft Offices in Reading (TVP) Memphis room

Speakers:

Jonathan Medd on the AD cmdlets in Windows Server 2008 R2

Alan Renouf on VMWare’s VI toolkit

plus there will be an introduction to Regular Expressions in PowerShell

 

Don’t forget Thursdays Live Meeting

Technorati Tags: ,

PowerShell Modules II

Last time we created a script file with three functions

new-password which controls the creation of a new password

get-randchar which generates a random character from a given character set

add-character which adds one of more characters of a given type to the password using get-randchar

Oops I made the mistake of calling it add-characters in the original script.  The convention is that nouns are always singular.

At the moment our three functions are in a .ps1 file that we can dot source to load the functions.  This loads all three functions but I only really want new-password to be visible.

An easier way to load functionality is to use the Import-Module cmdlet.  First off we need to turn our script into a module.  Ok change the name from user.ps1 to user.psm1.  Now we can use

Import-Module ./user.psm1

to load the functions.  We can see what is loaded by using

PS> Get-Module

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     user                      {add-characters, new-password, get-randchar}

One of the good things about using modules is that Remove-Module will unload the functions.  So we’ll do that and have a look at what else might be available with modules

PS> Get-Command *module*

CommandType     Name
-----------     ----
Cmdlet          Export-ModuleMember
Cmdlet          Get-Module
Cmdlet          Import-Module
Function        Load-Modules
Cmdlet          New-Module
Cmdlet          New-ModuleManifest
Cmdlet          Remove-Module
Cmdlet          Test-ModuleManifest

Export-ModuleMember looks interesting.  It will control the module members such as functions, aliases and variables that are exported by the module – that is they are made available. So we add a last line to the module.

Export-ModuleMember -Function new-password

After importing the module we have

PS> Get-Module

ModuleType Name                      ExportedCommands
---------- ----                      ----------------
Script     user                      new-password

If we do dir function:  we will only see our new-password function

By using modules we can control which functions are “published” and which functions remain in the background as invisible worker functions. As we add more functions into our module we just add to the list of functions being exported.

The module can be made even more flexible by creating a module manifest which is the subject of the next post.

 

Technorati Tags: ,,,
Posted by Richard Siddaway's Blog
Filed under:

Thursday – Live Meeting

Don’t forget

7pm GMT on Thursday February 26th sees Rolf Masuch presenting on using PowerShell as Active Directory Login Script.  Details here http://richardsiddaway.spaces.live.com/default.aspx?_c01_BlogPart=blogentry&_c=BlogPart&handle=cns!43CFA46A74CF3E96!2079 

 

Technorati Tags: ,

PowerShell advanced function parameters

One annoying error I have just stumbled over.  When using the Parameter keyword don’t put a space after it.  If you do this

[Parameter (Position=0,HelpMessage="The length of password. Default is random between 8 and 12")]

you will get this error

Cannot find type for custom attribute 'Parameter '. Please make sure the assembly containing this type is loaded.
At C:\Scripts\wip\password.ps1:29 char:20
+         [Parameter  <<<< (Position=0,HelpMessage="The length of password. Default is random between 8 and 12")]
    + CategoryInfo          : InvalidOperation: (Parameter :Token) [], RuntimeException
    + FullyQualifiedErrorId : CustomAttributeTypeNotFound

for it to run correctly you need to use

[Parameter(Position=0,HelpMessage="The length of password. Default is random between 8 and 12")]

notice that the opening ( comes immediately after the word Parameter

One to watch

 

Posted by Richard Siddaway's Blog
Filed under:

PowerShell ISE syntax errors

One thing I’ve noticed with the ISE is that if you have a syntax error such as mismatched brackets or forget a closing “ on a string all of the colour syntax highlighting disappears after the error – the text is all black (or whatever colour you are using).  Helps spot the errors.

 

Technorati Tags: ,,
Posted by Richard Siddaway's Blog
Filed under:

PowerShell v1 to v2 changes

Jeffrey left a comment on this post http://richardsiddaway.spaces.live.com/blog/cns!43CFA46A74CF3E96!2084.entry regarding discovery of WMI classes.  He pointed out that if the line of PowerShell is changed to

001
Get-WMIObject -NameSpace root -Recurse -List *Printer*

 

that you can check all namespaces in one hit.  Recurse! where did that come from. 

Checked the help file for Get-WmiObject and couldn’t see anything about recurse – but it is mentioned in the release notes. Must read the release notes more carefully.

There are a ton of changes between v1 and v2 and it can be difficult to keep track of them all.  One thing I do is download the v1 graphical help from http://www.microsoft.com/technet/scriptcenter/topics/winpsh/pschm.mspx and keep it on the desk top.  It is always handy to refer back to the v1 help to check for changes and I can compare the help for v1 and v2 easily.  Note that the v2 help isn’t always complete – functionality has to be produced before it can be documented.

The other good source of information is http://www.nivot.org/2009/02/04/DifferencesBetweenPowerShell10RTMAndPowershell20CTP3Win7Beta.aspx and http://www.nivot.org/2008/12/23/PowerShell20CTP3HasArrived.aspx

PowerShell is getting bigger and better every time we turn around. Hopefully these resources will help to keep track of the changes.

 

Technorati Tags: ,,
Posted by Richard Siddaway's Blog
Filed under:

Discovering WMI

One thing that seems to come up rather frequently on the newsgroups is what WMI class do I need to use to do X.

The really confusing thing about WMI is knowing just what is available. I have done far more with WMI since discovering PowerShell than I ever did with VBScript. That’s for 2 reasons. Firstly WMI is massively easier to use with PowerShell and it is easier to find out what you need to use.

WMI is arranged in a hierarchy of namespaces.  Most of the useful stuff can be found in the default namespace ‘root\cimv2’  We can find out what is available quite easily by using

001
Get-WmiObject -List

 

This will generate a long list of classes.  If you have an idea of what you want to look at then we can try something like

001
Get-WmiObject -List *printer*

 

which will return all of the classes relating to printers.  WMI classes are named fairly sensibly so it is easy to vary the search if you don’t quite get what you need.

Other good ways for discovering WMI – use PowerGUI – it has a WMI browser or download wmiexplorer from http://thepowershellguy.com/blogs/posh/archive/2007/03/22/powershell-wmi-explorer-part-1.aspx

Which ever way you decide to follow to discover WMI – it will be a big part of your PowerShell tool kit.

 

Technorati Tags: ,

Learning PowerShell – 4 friends

When you start learning PowerShell you will find that it has a massive amount of self discovery built into the system. There are four cmdlets you find yourself using all of the time:

  • Get-Help
  • Get-Command
  • Get-Member
  • Get-PSDrive

Oh.  You want to know what they do – try this

001
"get-help", "get-command", "get-member", "get-psdrive" | 
foreach { Get-Help $_ | select Name, Synopsis | Format-List}

OK this is playing to a certain extent but there are some interesting PowerShell points from this.

We start by creating a list of the cmdlets we want to investigate. These are passed onto the pipeline. Foreach will work with each one of them in turn as it comes along the pipeline. The next very important point is that we have a seperate pipeline within the foreach.

On the inner pipeline we start by using get help with the cmdlet name represented by $_  (you will see a lot of this guy – it represents the object on the pipeline).  We then select the name and the synopsis out of help and use format-list to display.

For detailed investigation of the cmdlets use the full and detailed parameters

001
002
Get-Help Get-Command -Detailed
Get-Help Get-Command -Full

Between these four friends you will be able to learn a lot about PowerShell

 

Technorati Tags: ,
Posted by Richard Siddaway's Blog
Filed under:

UK PowerShell UG meetings

Speakers for the next two meetings have been finalised.

In March we will be bringing you:

Jonathan Medd on the AD cmdlets in Windows Server 2008 R2

Alan Renouf on VMWare’s VI toolkit

plus there will be an introduction to Regular Expressions in PowerShell

 

In May (date tba in week of May 18-21)

Dmitry Sotnokov of PowerGUI fame will be joining us to speak about PowerGUI and other PowerShell related topics

 

Technorati Tags: ,

Learning PowerShell

One of the subjects that came up at the PowerShell event last week was how do you go about learning PowerShell.

If I was starting from scratch there are a number of things I would do:

  1. Download and install PowerShell  - use a test environment.
  2. Read the documentation that comes with PowerShell
    • The Getting Started and User Guide are a very good place to begin
  3. Download and read Frank Koch’s free PowerShell books from  http://blogs.technet.com/chitpro-de/archive/2008/02/28/free-windows-powershell-workbook-server-administration.aspx
  4. Don’t try and learn everything about PowerShell at once. Think about problems you need to solve. Think about how PowerShell can help.  Try to create a script to solve it. Use the Newsgroups to help. Repeat as necessary
  5. Get involved in the PowerShell community.  Join in on the newsgroups and share your scripts.

Hope this helps.  There are other steps that come after this and I’ll cover them later.  Point 4 is probably the most important. Put PowerShell to work for you as soon as you can – you get more out of it that way and the time you spend learning will rapidly be repaid.

 

Technorati Tags: ,

Posted by Richard Siddaway's Blog
Filed under:

Common parameters

I’ve been looking at the get-diskfreespace function over a number of posts. We’ve been looking a mixture of turning it into a production script and adding the advanced functionality from PowerShell v2.  In this post http://richardsiddaway.spaces.live.com/default.aspx?_c01_BlogPart=blogentry&_c=BlogPart&handle=cns!43CFA46A74CF3E96!2005 we added the

[CmdletBinding()] parameter

As soon as we add this parameter we get a whole bunch of functionality for free.  Our original function had a single parameter for the comptuer name – we now have a whole bunch of them

Get-Command get-diskfreespace | select parametersets

{ [[-computer] ] [-Verbose] [-Debug] [-ErrorAction ] [-WarningAction ] [-ErrorVariable ] [-WarningVariable ]

[-OutVariable ] [-OutBuffer ] }

These are the standard common parameters that we expect to see with any cmdlet.

Next we’ll  look at how we use some of them

 

Posted by Richard Siddaway's Blog
Filed under:

More colour syntax highlights

In this post - http://richardsiddaway.spaces.live.com/default.aspx?_c01_BlogPart=blogentry&_c=BlogPart&handle=cns!43CFA46A74CF3E96!2025 – I mentioned I had been experimenting with the copy script functionality so that I would get colour copy\paste from PowerShell ISE.  There was a problem in that I couldn’t paste into Live Writer – it crashed.

Lee Holmes has re-written the script -http://www.leeholmes.com/blog/MorePowerShellSyntaxHighlighting.aspx

One slight issue is that you need to create a profile for ISE – called Microsoft.PowershellISE_profile.ps1.  It goes in the WindowsPowerShell folder in the Documents folder of your profile.

In that profile you need the line 

001
002
## add the copy-script function
$psise.CustomMenu.Submenus.Add("Copy Script", {C:\Scripts\ISE\copy-script.ps1}, "Shift+Ctrl+Z")

This will add the script onto the custom menu in ISE. 

When you come to paste into Live Writer – use Edit – Paste Special – Keep Formatting  to get the colour syntax highlights.  Notice in this version that you get line numbers as well.  This will be useful for writing about the scripts

This seems to work OK – I’ll experiment so more and report back.

 

Technorati Tags: ,,

Enter the Clones

Following my comments about PowerShell being designed for the Jedi - http://richardsiddaway.spaces.live.com/default.aspx?_c01_BlogPart=blogentry&_c=BlogPart&handle=cns!43CFA46A74CF3E96!2066 – I discovered the Clones are now getting into the action - http://www.leeholmes.com/blog/MakingPerfectChangeWithTheFewestCoins.aspx

Lee (of Cookbook fame) has a very nice script for working out the smallest number of coins needed to make perfect change ie meet the price without needing change back.

The script makes very clever use of the Clone() method of a hash table – a clone can be regarded as a copy of the object in this case – and then builds an array of hash tables to hold the results.  For each possible value of 1 – 99 a hash table is created showing which coins are required to created that number of cents, pence or whatever

I really recommend that you have a look at this technique.

If you want convert the script to use any other currency just change the hash table – for instance for UK currency it becomes

$coins = @{ 0.50 = 0; 0.20 = 0; 0.10 = 0; 0.05 = 0; 0.02 = 0; 0.01 = 0 }

The other clever bit is the way the results are presented to automatically include the correct currency symbol.   In case you are wondering the results for the UK are

£0.50: 1
£0.20: 2
£0.10: 1
£0.05: 1
£0.02: 2
£0.01: 1

We only need 8 coins to make any amount of change below £1

 

Technorati Tags:

Posted by Richard Siddaway's Blog
Filed under:

Cultured Dates

In this post - http://richardsiddaway.spaces.live.com/default.aspx?_c01_BlogPart=blogentry&_c=BlogPart&handle=cns!43CFA46A74CF3E96!2007 – I talked about the way date strings had to be formatted when we were creating datetime objects -

[datetime]”mm/dd/yyyyy”

I have since discovered its a little bit more complicated than that. 

If we start by examining the culture on my machine.

PS> $host | Select *culture | fl

CurrentCulture   : en-GB
CurrentUICulture : en-US

Notice they are not the same – en-US means I have to use American (US) English for UI based activities but the culture of the machine is set to en-GB which is UK English. To recap the previous post if I am using [datetime] to create the object I need to follow the UICulture – that is use a mm//dd/yyyy format as shown

PS> [datetime]"25/12/2009"
Cannot convert value "25/12/2009" to type "System.DateTime". Error: "String was not recognized as a valid DateTime."
At line:1 char:11
+ [datetime] <<<< "25/12/2009"
    + CategoryInfo          : NotSpecified: (:) [], RuntimeException
    + FullyQualifiedErrorId : RuntimeException

PS> [datetime]"12/25/2009"

25 December 2009 00:00:00

If I use get-date to create the object – it doesn’t work using the same rules

PS> Get-Date -Date "12/25/2009"
Get-Date : Cannot bind parameter 'Date'. Cannot convert value "12/25/2009" to type "System.DateTime". Error: "String was not recognized as a valid DateTime."
At line:1 char:15
+ Get-Date -Date <<<<  "12/25/2009"
    + CategoryInfo          : InvalidArgument: (:) [Get-Date], ParameterBindingException
    + FullyQualifiedErrorId : CannotConvertArgumentNoMessage,Microsoft.PowerShell.Commands.GetDateCommand

It does work with the CurrentCulture format – ie UK format of dd/mm/yyyyy

PS> Get-Date -Date "25/12/2009"

25 December 2009 00:00:00

PS> Get-Date "25/12/2009"

25 December 2009 00:00:00

PS> Get-Date 25/12/2009

25 December 2009 00:00:00

Just to complete the set you can also create dates like this

PS> New-Object -TypeName System.DateTime -ArgumentList 2009,12,25

25 December 2009 00:00:00

Where we use year, month and day as the 3 arguments – this ignores culture completely.

If you are using PowerShell with a machine set for US English [datetime] and get-date work in an identical manner – otherwise be aware that the machine culture will affect how get-date works in this instance.

 

Technorati Tags: ,
Posted by Richard Siddaway's Blog
Filed under:

Machine UpTime

The February copy of TechNet magazine dropped through the letter box this morning. In the UK we get our own version so some of this may be a bit older than this months edition.  There is a nice article by Marco Shaw on using PowerShell with System Center Operations Manager that is well worth reading.

The article that really got me thinking was the one about calculating server uptime using information from the event logs. The script is actually measuring the availability of the event log service but it is very close to the available time.

One thing that really leapt out was that the main script was using PowerShell v2 – it had a #Requires –version 2.0

statement at the top.  As v2 is still in CTP that didn’t seem right.  The whole script looked over complicated so I started playing around and came up with this:

$days = 30
$now = Get-Date
$start = (Get-Date -Hour 00 -Minute 00 -Second 00).AddDays(-$days)
"Checking Last Boot Time"
$os = Get-WmiObject -Class Win32_OperatingSystem
$lastboot = $os.ConvertToDateTime($os.LastBootUpTime)
if ($lastboot -lt $start){ Write-Host "Server continually up for whole period"; Return}
else {Write-Host "Server restarted since start of period - analysis continuing"}

"Reading Event Logs"
$events = Get-EventLog -LogName system | where{(($_.EventId -eq 6005) -or ($_.EventId -eq 6006)) -and $_.TimeGenerated -ge $start } | Select EventId, TimeGenerated, Index

## should start with a 6005 - log service started event
if ($events[0].EventId -eq 6005){
    $totaluptime = $now - $events[0].Timegenerated
}
else {
    Write-Host "Error reading log - startup is not first entry"
    Return
}

#check the last
$last = $events | select -Last 1
if ($last.EventId -eq 6006){      ## shutdown
    $totaluptime += ($last.TimeGenerated - $start)
}

## events should be paired 6006\6005 shutdown & start respectively
for ($i = 1; $i -le $events.count-2; $i += 2){
    if ($events[$i].EventId -eq 6006){      ## shutdown
        if ($events[$i+1].EventId -eq 6005){      ## Startup
            $totaluptime += ($events[$i].Timegenerated - $events[$i+1].Timegenerated)
        }
        else {
            Write-Host "Error in log sequence at " $event[$i+1]
            Return    
        }
    }
    else {
        Write-Host "Error in log sequence at " $event[$i]
        Return    
    }
}
## calculate uptime
$totaltime = $now - $start
$percUptime = (($totaltime.TotalHours - $totaluptime.TotalHours)/$totaltime.TotalHours)*100

"Uptime for period $($start.ToLongDateString()) to $($now.ToLongDateString())"
"Total time available: {0:n2} hours" -f $($totaltime.TotalHours)
"Total Uptime: {0:n2} hours" -f $($totaluptime.TotalHours)
"Percentage Uptime: {0:n2} %" -f $percUptime
"Percentage Downtime: {0:n2} %" -f (100 - $percUptime)

 

We start by defining some variables – the number of days we want to analyse, current date and our starting point.

The first check is on when the server was actually started – if it was before the beginning of our period then we have 100% up time and the bonus is in the bank.  We can check this using WMI.  The only awkward bit is converting the boot time to a format we can work with.

Assuming that our server was started since the start of out analysis period then we need to look at the logs.  We can read the system event log looking for eventids 6005 and 6006 as shown. We only want events since the start of our period.

The event logs are writing in chronological order and are returned in the same order with the youngest returned first.  I have yet to see an instance of pulling information from the logs when this wasn’t the case.

The first (youngest) event should be a 6005 – event log service started.  We create a timespan by subtracting that time from the current time which gives us up time since the last restart. If this isn’t the case then we have a problem that needs to be investigated so the script stops.

A check is made to see if the last event is a shutdown – in which case we need to calculate the uptime from the start of the period to shutdown and add it to the total.

The 6005\6006 events should be paired after this with a 6006 (shutdown) followed by a 6005 (startup) – remember we are working backwards in time.  Assuming we find our pairs of events as expected we calculate the timespan between the events and add it to our total uptime.  If the pairings don’t match up then we have an issue to be investigated so the event information is written to screen to give a starting point for analysis.

We then calculate the total timespan of our period and the percentage uptime.  Finally we print out our results.

I think this is easier to follow and seems to work correctly in my testing environment. Remember that this runs on the local machine as written.  It can be made to work on remote machines – Get-WmiObject accepts a computer name parameter as does get-eventlog in PowerShell v2.  If you are using v1 then you could access the remote logs using WMI.

 

Posted by Richard Siddaway's Blog
Filed under:

Why Powershell is for the Jedi

It just has to be.  Do you know how many cmdlets use the force?  In CTP 3 try

get-help * -Parameter Force

 

Technorati Tags: ,
Posted by Richard Siddaway's Blog
Filed under:

Friday 13th

As today is Friday 13th with all of the implications that holds – who me superstitious? – I thought I would find out other days of the year I needed to hibernate

function fri13 {
param ($year)
    for ($m=1; $m -le 12; $m++) {
        $d = [datetime]"$m/13/$year"
        if ($d.DayOfWeek -eq "Friday"){
            $d.ToLongDateString()
        }
    }
}

The function takes a year as the parameter and iterates through the months building dates for the 13th of each month. Remember that the date is always mm/dd/yyyy format when creating dates like this.  The date is built using string substitution – that is just so useful.

A check on the day of the week for the date and if is a friday the date is listed out.

The function is called as

fri13 2009 

If you want to check for a number of years then we can use a loop

for ($y=2009; $y -le 2014; $y++) {  fri13 $y}

Just in case you were wondering – these are the days to be aware of

13 February 2009
13 March 2009
13 November 2009
13 August 2010
13 May 2011
13 January 2012
13 April 2012
13 July 2012
13 September 2013
13 December 2013
13 June 2014

Lets be careful out there.

 

Technorati Tags: ,
Posted by Richard Siddaway's Blog
Filed under:
More Posts Next page »